From 20cb29c2f8c5c87bc590896854a20b1473ceb358 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Sat, 8 Aug 2026 03:54:55 +0000 Subject: #2 --- .cursor/rules/hybbx-c.mdc | 22 + .cursor/rules/hybbx-project.mdc | 35 + .cursor/rules/public-product-terminology.mdc | 31 + .editorconfig | 23 + .gitignore | 29 + AGENTS.md | 65 + CHANGELOG.md | 57 + CMakeLists.txt | 272 ++ CONTRIBUTING.md | 29 + LICENSE.txt | 691 +++++ README.md | 70 + cmake/Compiler.cmake | 97 + cmake/Hardening.cmake | 44 + cmake/HyBBXConfig.cmake.in | 11 + cmake/Toolchain-AmigaOS.cmake | 39 + cmake/Toolchain-Clang.cmake | 6 + docs/ARDOP.md | 27 + docs/BAYCOM.md | 57 + docs/BUILD.md | 44 + docs/CLIENTS.md | 28 + docs/COMMANDS.md | 36 + docs/CRDOP.md | 35 + docs/DEVELOPMENT.md | 37 + docs/DUAL-TNC-OPERATOR-GUIDE.md | 179 ++ docs/ENTERTAIN.md | 25 + docs/LICENSING.md | 26 + docs/MAINS_PROXY.md | 37 + docs/MANUAL.md | 63 + docs/MASTER-GUIDE.md | 211 ++ docs/PLATFORMS.md | 28 + docs/README.md | 32 + docs/SECURITY.md | 38 + docs/TMODEM.md | 32 + docs/TNCS.md | 56 + docs/TOPOLOGY.md | 43 + docs/WEBSOCKET.md | 27 + include/hybbx/ardop.h | 50 + include/hybbx/auth.h | 117 + include/hybbx/ax25.h | 68 + include/hybbx/bandwidth_policy.h | 35 + include/hybbx/baycom.h | 82 + include/hybbx/broadcast.h | 110 + include/hybbx/chat.h | 82 + include/hybbx/circuit.h | 119 + include/hybbx/circuit_balance.h | 131 + include/hybbx/circuit_bridge.h | 46 + include/hybbx/circuit_tcp.h | 162 ++ include/hybbx/command.h | 71 + include/hybbx/commands_registry.h | 73 + include/hybbx/conference.h | 69 + include/hybbx/config.h | 126 + include/hybbx/crdop.h | 44 + include/hybbx/crypto.h | 101 + include/hybbx/crypto_config.h | 69 + include/hybbx/daemon_wrap.h | 36 + include/hybbx/hybbx.h | 10 + include/hybbx/instance.h | 61 + include/hybbx/kiss.h | 61 + include/hybbx/limits.h | 136 + include/hybbx/link.h | 86 + include/hybbx/log.h | 64 + include/hybbx/mail.h | 129 + include/hybbx/mains_proxy.h | 120 + include/hybbx/max25.h | 72 + include/hybbx/messages.h | 58 + include/hybbx/monitor.h | 74 + include/hybbx/networks.h | 63 + include/hybbx/packet_radio.h | 57 + include/hybbx/password.h | 53 + include/hybbx/plugin.h | 75 + include/hybbx/posix_time.h | 21 + include/hybbx/privilege.h | 24 + include/hybbx/proxychat.h | 40 + include/hybbx/proxymail.h | 73 + include/hybbx/registry.h | 28 + include/hybbx/rf_tx_pace.h | 16 + include/hybbx/security.h | 31 + include/hybbx/security_ban.h | 80 + include/hybbx/service.h | 174 ++ include/hybbx/session.h | 226 ++ include/hybbx/socket.h | 35 + include/hybbx/ssh.h | 57 + include/hybbx/storage.h | 178 ++ include/hybbx/telnet.h | 40 + include/hybbx/terminal.h | 39 + include/hybbx/texts.h | 84 + include/hybbx/tnc.h | 240 ++ include/hybbx/tnc2c.h | 27 + include/hybbx/traffic.h | 59 + include/hybbx/types.h | 42 + include/hybbx/util.h | 143 + include/hybbx/websocket.h | 53 + plugins/CMakeLists.txt | 44 + plugins/ardop/CMakeLists.txt | 57 + plugins/ardop/ardop.c | 49 + plugins/ardop/ardop_config.c | 186 ++ plugins/ardop/ardop_crc.c | 69 + plugins/ardop/ardop_crc.h | 15 + plugins/ardop/ardop_host.c | 650 +++++ plugins/ardop/ardop_host.h | 47 + plugins/ardop/ardop_link.c | 446 +++ plugins/ardop/ardop_link.h | 37 + plugins/baycom/CMakeLists.txt | 36 + plugins/baycom/baycom.c | 623 +++++ plugins/baycom/baycom_config.c | 395 +++ plugins/baycom/baycom_kernel.c | 472 ++++ plugins/baycom/baycom_kernel.h | 26 + plugins/baycom/baycom_kiss.c | 262 ++ plugins/baycom/baycom_kiss.h | 26 + plugins/baycom/baycom_modem.c | 94 + plugins/baycom/baycom_modem.h | 31 + plugins/crdop/CMakeLists.txt | 26 + plugins/crdop/crdop.c | 51 + plugins/crdop/crdop_config.c | 186 ++ plugins/mains_proxy/CMakeLists.txt | 23 + plugins/mains_proxy/mains_proxy.c | 137 + plugins/packet_radio/CMakeLists.txt | 36 + plugins/packet_radio/ax25.c | 250 ++ plugins/packet_radio/kiss.c | 152 + plugins/packet_radio/packet_radio.c | 1005 +++++++ plugins/packet_radio/packet_radio_config.c | 786 ++++++ plugins/packet_radio/serial_port.c | 705 +++++ plugins/packet_radio/serial_port.h | 52 + plugins/packet_radio/sixpack.c | 146 + plugins/packet_radio/sixpack.h | 40 + plugins/packet_radio/tnc.c | 1080 +++++++ plugins/packet_radio/tnc_host.c | 143 + plugins/packet_radio/tnc_host.h | 51 + plugins/packet_radio/tnc_profiles.c | 399 +++ plugins/ssh/CMakeLists.txt | 35 + plugins/ssh/ssh.c | 728 +++++ plugins/ssh/ssh_config.c | 132 + plugins/ssh/ssh_keys.c | 146 + plugins/telnet/CMakeLists.txt | 26 + plugins/telnet/telnet.c | 516 ++++ plugins/telnet/telnet_config.c | 125 + plugins/telnet/telnet_proto.c | 213 ++ plugins/telnet/telnet_proto.h | 37 + plugins/websocket/CMakeLists.txt | 37 + plugins/websocket/websocket.c | 723 +++++ plugins/websocket/ws_config.c | 163 ++ plugins/websocket/ws_proto.c | 565 ++++ plugins/websocket/ws_proto.h | 63 + plugins/websocket/ws_sha1.c | 138 + plugins/websocket/ws_sha1.h | 9 + plugins/websocket/ws_tls.c | 400 +++ plugins/websocket/ws_tls.h | 43 + scripts/build-amiga-telnet.sh | 26 + scripts/build-clients.sh | 19 + scripts/dev-setup.sh | 19 + scripts/hybbx.sh | 86 + scripts/mock-ardopc.py | 137 + scripts/test-ardop-plugin.sh | 89 + scripts/test-crdop-plugin.sh | 97 + share/THIRD_PARTY_NOTICES.txt | 22 + share/areas.yaml | 66 + share/cgi/hybbx-telnet.cgi.example | 18 + share/commands.yaml | 306 ++ share/data.gitkeep | 0 share/fail2ban/filter.d/hybbx-circuit.conf | 10 + share/fail2ban/filter.d/hybbx-ssh.conf | 11 + share/fail2ban/filter.d/hybbx-telnet.conf | 10 + share/fail2ban/filter.d/hybbx-websocket.conf | 11 + share/fail2ban/jail.d/hybbx.local.example | 45 + share/hybbx-main.ini.example | 97 + share/hybbx-mesh.ini.example | 93 + share/hybbx-secondary.ini.example | 64 + share/hybbx-standalone.ini.example | 125 + share/hybbx.ini.example | 17 + share/hybbxd-main.ini.example | 82 + share/hybbxpd-proxy.ini.example | 61 + share/hybbxsd-secondary.ini.example | 41 + share/logs.gitkeep | 0 share/reverse-proxy/README.txt | 6 + share/reverse-proxy/apache2.conf.example | 24 + share/reverse-proxy/docroot/README.txt | 7 + .../docroot/hybbx-websocket/hybbx-terminal.js | 95 + .../docroot/hybbx-websocket/index.php | 89 + share/reverse-proxy/lighttpd.conf.example | 26 + share/reverse-proxy/nginx.conf.example | 28 + src/CMakeLists.txt | 204 ++ src/clients/CMakeLists.txt | 96 + src/clients/circuit_client.c | 294 ++ src/clients/client_display.c | 87 + src/clients/client_display.h | 30 + src/clients/client_line.c | 495 ++++ src/clients/client_line.h | 33 + src/clients/hybbx_ssh.c | 495 ++++ src/clients/hybbx_telnet.c | 544 ++++ src/clients/hybbx_terminal.c | 433 +++ src/clients/link_auth.c | 182 ++ src/core/auth.c | 477 ++++ src/core/bandwidth_policy.c | 229 ++ src/core/broadcast.c | 982 +++++++ src/core/chat.c | 463 +++ src/core/circuit.c | 337 +++ src/core/circuit_balance.c | 545 ++++ src/core/circuit_bridge.c | 175 ++ src/core/circuit_tcp.c | 1837 ++++++++++++ src/core/command.c | 2450 ++++++++++++++++ src/core/command_parse.c | 347 +++ src/core/commands_registry.c | 1429 ++++++++++ src/core/conference.c | 720 +++++ src/core/config.c | 862 ++++++ src/core/crdop.c | 66 + src/core/crypto.c | 302 ++ src/core/crypto_backends.c | 223 ++ src/core/crypto_backends.h | 62 + src/core/crypto_config.c | 247 ++ src/core/crypto_libsodium.c | 111 + src/core/crypto_openssl.c | 142 + src/core/daemon_wrap.c | 320 +++ src/core/instance.c | 272 ++ src/core/link.c | 427 +++ src/core/log.c | 694 +++++ src/core/mail.c | 1532 ++++++++++ src/core/mail_sql.c | 655 +++++ src/core/mail_sql.h | 40 + src/core/mains_proxy.c | 1034 +++++++ src/core/max25.c | 507 ++++ src/core/messages.c | 110 + src/core/monitor.c | 497 ++++ src/core/networks.c | 191 ++ src/core/password.c | 396 +++ src/core/privilege.c | 233 ++ src/core/proxychat.c | 134 + src/core/proxymail.c | 676 +++++ src/core/registry.c | 83 + src/core/rf_tx_pace.c | 100 + src/core/security.c | 205 ++ src/core/security_ban.c | 1213 ++++++++ src/core/service.c | 1270 +++++++++ src/core/session.c | 2678 ++++++++++++++++++ src/core/storage.c | 378 +++ src/core/storage_flatfile.c | 1507 ++++++++++ src/core/storage_private.h | 81 + src/core/storage_sql.c | 1209 ++++++++ src/core/terminal.c | 98 + src/core/texts.c | 470 ++++ src/core/traffic.c | 294 ++ src/core/util.c | 832 ++++++ src/instance_role_main.c | 7 + src/instance_role_proxy.c | 7 + src/instance_role_secondary.c | 7 + src/main.c | 371 +++ tests/CMakeLists.txt | 33 + tests/test_config.c | 60 + tests/test_instance.c | 170 ++ tests/test_mains_proxy.c | 189 ++ tests/test_mains_proxy_e2e.c | 360 +++ tests/test_storage_smoke.c | 235 ++ tests/test_util.c | 152 + text/banner.txt | 4 + text/motd.txt | 5 + text/news.txt | 1 + text/rules.txt | 50 + text/version.txt | 2 + third_party/monocypher/monocypher.c | 2956 ++++++++++++++++++++ third_party/monocypher/monocypher.h | 321 +++ third_party/tinyaes/aes.c | 572 ++++ third_party/tinyaes/aes.h | 97 + third_party/tinyaes/aes256gcm.c | 267 ++ third_party/tinyaes/aes256gcm.h | 32 + third_party/tinyaes/hybbx_aes_config.h | 13 + third_party/tinysha256/README.md | 8 + third_party/tinysha256/tinysha256.c | 210 ++ third_party/tinysha256/tinysha256.h | 30 + 267 files changed, 60955 insertions(+) create mode 100644 .cursor/rules/hybbx-c.mdc create mode 100644 .cursor/rules/hybbx-project.mdc create mode 100644 .cursor/rules/public-product-terminology.mdc create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 cmake/Compiler.cmake create mode 100644 cmake/Hardening.cmake create mode 100644 cmake/HyBBXConfig.cmake.in create mode 100644 cmake/Toolchain-AmigaOS.cmake create mode 100644 cmake/Toolchain-Clang.cmake create mode 100644 docs/ARDOP.md create mode 100644 docs/BAYCOM.md create mode 100644 docs/BUILD.md create mode 100644 docs/CLIENTS.md create mode 100644 docs/COMMANDS.md create mode 100644 docs/CRDOP.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/DUAL-TNC-OPERATOR-GUIDE.md create mode 100644 docs/ENTERTAIN.md create mode 100644 docs/LICENSING.md create mode 100644 docs/MAINS_PROXY.md create mode 100644 docs/MANUAL.md create mode 100644 docs/MASTER-GUIDE.md create mode 100644 docs/PLATFORMS.md create mode 100644 docs/README.md create mode 100644 docs/SECURITY.md create mode 100644 docs/TMODEM.md create mode 100644 docs/TNCS.md create mode 100644 docs/TOPOLOGY.md create mode 100644 docs/WEBSOCKET.md create mode 100644 include/hybbx/ardop.h create mode 100644 include/hybbx/auth.h create mode 100644 include/hybbx/ax25.h create mode 100644 include/hybbx/bandwidth_policy.h create mode 100644 include/hybbx/baycom.h create mode 100644 include/hybbx/broadcast.h create mode 100644 include/hybbx/chat.h create mode 100644 include/hybbx/circuit.h create mode 100644 include/hybbx/circuit_balance.h create mode 100644 include/hybbx/circuit_bridge.h create mode 100644 include/hybbx/circuit_tcp.h create mode 100644 include/hybbx/command.h create mode 100644 include/hybbx/commands_registry.h create mode 100644 include/hybbx/conference.h create mode 100644 include/hybbx/config.h create mode 100644 include/hybbx/crdop.h create mode 100644 include/hybbx/crypto.h create mode 100644 include/hybbx/crypto_config.h create mode 100644 include/hybbx/daemon_wrap.h create mode 100644 include/hybbx/hybbx.h create mode 100644 include/hybbx/instance.h create mode 100644 include/hybbx/kiss.h create mode 100644 include/hybbx/limits.h create mode 100644 include/hybbx/link.h create mode 100644 include/hybbx/log.h create mode 100644 include/hybbx/mail.h create mode 100644 include/hybbx/mains_proxy.h create mode 100644 include/hybbx/max25.h create mode 100644 include/hybbx/messages.h create mode 100644 include/hybbx/monitor.h create mode 100644 include/hybbx/networks.h create mode 100644 include/hybbx/packet_radio.h create mode 100644 include/hybbx/password.h create mode 100644 include/hybbx/plugin.h create mode 100644 include/hybbx/posix_time.h create mode 100644 include/hybbx/privilege.h create mode 100644 include/hybbx/proxychat.h create mode 100644 include/hybbx/proxymail.h create mode 100644 include/hybbx/registry.h create mode 100644 include/hybbx/rf_tx_pace.h create mode 100644 include/hybbx/security.h create mode 100644 include/hybbx/security_ban.h create mode 100644 include/hybbx/service.h create mode 100644 include/hybbx/session.h create mode 100644 include/hybbx/socket.h create mode 100644 include/hybbx/ssh.h create mode 100644 include/hybbx/storage.h create mode 100644 include/hybbx/telnet.h create mode 100644 include/hybbx/terminal.h create mode 100644 include/hybbx/texts.h create mode 100644 include/hybbx/tnc.h create mode 100644 include/hybbx/tnc2c.h create mode 100644 include/hybbx/traffic.h create mode 100644 include/hybbx/types.h create mode 100644 include/hybbx/util.h create mode 100644 include/hybbx/websocket.h create mode 100644 plugins/CMakeLists.txt create mode 100644 plugins/ardop/CMakeLists.txt create mode 100644 plugins/ardop/ardop.c create mode 100644 plugins/ardop/ardop_config.c create mode 100644 plugins/ardop/ardop_crc.c create mode 100644 plugins/ardop/ardop_crc.h create mode 100644 plugins/ardop/ardop_host.c create mode 100644 plugins/ardop/ardop_host.h create mode 100644 plugins/ardop/ardop_link.c create mode 100644 plugins/ardop/ardop_link.h create mode 100644 plugins/baycom/CMakeLists.txt create mode 100644 plugins/baycom/baycom.c create mode 100644 plugins/baycom/baycom_config.c create mode 100644 plugins/baycom/baycom_kernel.c create mode 100644 plugins/baycom/baycom_kernel.h create mode 100644 plugins/baycom/baycom_kiss.c create mode 100644 plugins/baycom/baycom_kiss.h create mode 100644 plugins/baycom/baycom_modem.c create mode 100644 plugins/baycom/baycom_modem.h create mode 100644 plugins/crdop/CMakeLists.txt create mode 100644 plugins/crdop/crdop.c create mode 100644 plugins/crdop/crdop_config.c create mode 100644 plugins/mains_proxy/CMakeLists.txt create mode 100644 plugins/mains_proxy/mains_proxy.c create mode 100644 plugins/packet_radio/CMakeLists.txt create mode 100644 plugins/packet_radio/ax25.c create mode 100644 plugins/packet_radio/kiss.c create mode 100644 plugins/packet_radio/packet_radio.c create mode 100644 plugins/packet_radio/packet_radio_config.c create mode 100644 plugins/packet_radio/serial_port.c create mode 100644 plugins/packet_radio/serial_port.h create mode 100644 plugins/packet_radio/sixpack.c create mode 100644 plugins/packet_radio/sixpack.h create mode 100644 plugins/packet_radio/tnc.c create mode 100644 plugins/packet_radio/tnc_host.c create mode 100644 plugins/packet_radio/tnc_host.h create mode 100644 plugins/packet_radio/tnc_profiles.c create mode 100644 plugins/ssh/CMakeLists.txt create mode 100644 plugins/ssh/ssh.c create mode 100644 plugins/ssh/ssh_config.c create mode 100644 plugins/ssh/ssh_keys.c create mode 100644 plugins/telnet/CMakeLists.txt create mode 100644 plugins/telnet/telnet.c create mode 100644 plugins/telnet/telnet_config.c create mode 100644 plugins/telnet/telnet_proto.c create mode 100644 plugins/telnet/telnet_proto.h create mode 100644 plugins/websocket/CMakeLists.txt create mode 100644 plugins/websocket/websocket.c create mode 100644 plugins/websocket/ws_config.c create mode 100644 plugins/websocket/ws_proto.c create mode 100644 plugins/websocket/ws_proto.h create mode 100644 plugins/websocket/ws_sha1.c create mode 100644 plugins/websocket/ws_sha1.h create mode 100644 plugins/websocket/ws_tls.c create mode 100644 plugins/websocket/ws_tls.h create mode 100755 scripts/build-amiga-telnet.sh create mode 100755 scripts/build-clients.sh create mode 100755 scripts/dev-setup.sh create mode 100755 scripts/hybbx.sh create mode 100755 scripts/mock-ardopc.py create mode 100755 scripts/test-ardop-plugin.sh create mode 100755 scripts/test-crdop-plugin.sh create mode 100644 share/THIRD_PARTY_NOTICES.txt create mode 100644 share/areas.yaml create mode 100644 share/cgi/hybbx-telnet.cgi.example create mode 100644 share/commands.yaml create mode 100644 share/data.gitkeep create mode 100644 share/fail2ban/filter.d/hybbx-circuit.conf create mode 100644 share/fail2ban/filter.d/hybbx-ssh.conf create mode 100644 share/fail2ban/filter.d/hybbx-telnet.conf create mode 100644 share/fail2ban/filter.d/hybbx-websocket.conf create mode 100644 share/fail2ban/jail.d/hybbx.local.example create mode 100644 share/hybbx-main.ini.example create mode 100644 share/hybbx-mesh.ini.example create mode 100644 share/hybbx-secondary.ini.example create mode 100644 share/hybbx-standalone.ini.example create mode 100644 share/hybbx.ini.example create mode 100644 share/hybbxd-main.ini.example create mode 100644 share/hybbxpd-proxy.ini.example create mode 100644 share/hybbxsd-secondary.ini.example create mode 100644 share/logs.gitkeep create mode 100644 share/reverse-proxy/README.txt create mode 100644 share/reverse-proxy/apache2.conf.example create mode 100644 share/reverse-proxy/docroot/README.txt create mode 100644 share/reverse-proxy/docroot/hybbx-websocket/hybbx-terminal.js create mode 100644 share/reverse-proxy/docroot/hybbx-websocket/index.php create mode 100644 share/reverse-proxy/lighttpd.conf.example create mode 100644 share/reverse-proxy/nginx.conf.example create mode 100644 src/CMakeLists.txt create mode 100644 src/clients/CMakeLists.txt create mode 100644 src/clients/circuit_client.c create mode 100644 src/clients/client_display.c create mode 100644 src/clients/client_display.h create mode 100644 src/clients/client_line.c create mode 100644 src/clients/client_line.h create mode 100644 src/clients/hybbx_ssh.c create mode 100644 src/clients/hybbx_telnet.c create mode 100644 src/clients/hybbx_terminal.c create mode 100644 src/clients/link_auth.c create mode 100644 src/core/auth.c create mode 100644 src/core/bandwidth_policy.c create mode 100644 src/core/broadcast.c create mode 100644 src/core/chat.c create mode 100644 src/core/circuit.c create mode 100644 src/core/circuit_balance.c create mode 100644 src/core/circuit_bridge.c create mode 100644 src/core/circuit_tcp.c create mode 100644 src/core/command.c create mode 100644 src/core/command_parse.c create mode 100644 src/core/commands_registry.c create mode 100644 src/core/conference.c create mode 100644 src/core/config.c create mode 100644 src/core/crdop.c create mode 100644 src/core/crypto.c create mode 100644 src/core/crypto_backends.c create mode 100644 src/core/crypto_backends.h create mode 100644 src/core/crypto_config.c create mode 100644 src/core/crypto_libsodium.c create mode 100644 src/core/crypto_openssl.c create mode 100644 src/core/daemon_wrap.c create mode 100644 src/core/instance.c create mode 100644 src/core/link.c create mode 100644 src/core/log.c create mode 100644 src/core/mail.c create mode 100644 src/core/mail_sql.c create mode 100644 src/core/mail_sql.h create mode 100644 src/core/mains_proxy.c create mode 100644 src/core/max25.c create mode 100644 src/core/messages.c create mode 100644 src/core/monitor.c create mode 100644 src/core/networks.c create mode 100644 src/core/password.c create mode 100644 src/core/privilege.c create mode 100644 src/core/proxychat.c create mode 100644 src/core/proxymail.c create mode 100644 src/core/registry.c create mode 100644 src/core/rf_tx_pace.c create mode 100644 src/core/security.c create mode 100644 src/core/security_ban.c create mode 100644 src/core/service.c create mode 100644 src/core/session.c create mode 100644 src/core/storage.c create mode 100644 src/core/storage_flatfile.c create mode 100644 src/core/storage_private.h create mode 100644 src/core/storage_sql.c create mode 100644 src/core/terminal.c create mode 100644 src/core/texts.c create mode 100644 src/core/traffic.c create mode 100644 src/core/util.c create mode 100644 src/instance_role_main.c create mode 100644 src/instance_role_proxy.c create mode 100644 src/instance_role_secondary.c create mode 100644 src/main.c create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_config.c create mode 100644 tests/test_instance.c create mode 100644 tests/test_mains_proxy.c create mode 100644 tests/test_mains_proxy_e2e.c create mode 100644 tests/test_storage_smoke.c create mode 100644 tests/test_util.c create mode 100644 text/banner.txt create mode 100644 text/motd.txt create mode 100644 text/news.txt create mode 100644 text/rules.txt create mode 100644 text/version.txt create mode 100644 third_party/monocypher/monocypher.c create mode 100644 third_party/monocypher/monocypher.h create mode 100644 third_party/tinyaes/aes.c create mode 100644 third_party/tinyaes/aes.h create mode 100644 third_party/tinyaes/aes256gcm.c create mode 100644 third_party/tinyaes/aes256gcm.h create mode 100644 third_party/tinyaes/hybbx_aes_config.h create mode 100644 third_party/tinysha256/README.md create mode 100644 third_party/tinysha256/tinysha256.c create mode 100644 third_party/tinysha256/tinysha256.h diff --git a/.cursor/rules/hybbx-c.mdc b/.cursor/rules/hybbx-c.mdc new file mode 100644 index 0000000..acd8b08 --- /dev/null +++ b/.cursor/rules/hybbx-c.mdc @@ -0,0 +1,22 @@ +--- +description: C/H coding patterns for HyBBX core and plugins +globs: "**/*.{c,h}" +alwaysApply: false +--- + +# HyBBX C conventions + +```c +/* ✅ Public API in include/hybbx/ */ +hybbx_result_t hybbx_foo_do_thing(hybbx_foo_t *foo, const char *in); + +/* ✅ Booleans from INI / config strings */ +int enabled = hybbx_parse_bool(value, default_value); + +/* ✅ Safe copy */ +hybbx_strlcpy(dst, src, sizeof(dst)); +``` + +- Return `HYBBX_ERR_*` from [include/hybbx/types.h](../../include/hybbx/types.h); `HYBBX_OK` on success. +- Plugin wire code stays in `plugins/` — not `src/core/`. +- Packet radio: TNC/AX.25 in `plugins/packet_radio/`; HBX client to `[circuit]`. diff --git a/.cursor/rules/hybbx-project.mdc b/.cursor/rules/hybbx-project.mdc new file mode 100644 index 0000000..df97cf5 --- /dev/null +++ b/.cursor/rules/hybbx-project.mdc @@ -0,0 +1,35 @@ +--- +description: HyBBX project conventions for all work in this repo +alwaysApply: true +--- + +# HyBBX project rules + +Read [AGENTS.md](../../AGENTS.md) and [docs/DEVELOPMENT.md](../../docs/DEVELOPMENT.md). + +## Product + +Plugin-only session daemon; Main + Secondary HBX topology ([TOPOLOGY.md](../../docs/TOPOLOGY.md)). **v2.0.0**. Technical docs in `docs/`; root README is short overview. + +## Code + +- C99, `hybbx_` prefix, `hybbx_result_t` errors +- Core = sessions + HBX/TCP only — no wire-protocol parsing in `src/core/` +- Plugins in `plugins/`; booleans via `hybbx_parse_bool()` +- Minimal scope; [limits.h](../../include/hybbx/limits.h) +- **POSIX+ / portability:** shared code stays POSIX-friendly; keep `*BSD` (FreeBSD, NetBSD, OpenBSD, …) and AmigaOS 3.9+ compatible; platform code behind `_WIN32` / `__AMIGA__` guards or dedicated files + +## Docs + +- **Technical only** — operator/INI → MANUAL.md + `share/*.ini.example`; commands → COMMANDS.md + commands.yaml; build → BUILD.md +- No planning, roadmap, feature-status, or release-note docs +- **Current version only** — no prior-version references in docs, `text/`, or user-facing comments +- **As-is software** — compact prose; no upgrade/migration/history sections +- **Linux-based operator docs** — `docs/`, `text/`, README, share examples. `HTTPD_DOCROOT`, `systemctl`, `ss`. Platform targets only in `docs/PLATFORMS.md` — own sections per OS (Linux, AmigaOS 3.9+, *BSD, MacOS X+, Windows 10+, …); spell **MacOS**, not macOS +- **POSIX+ / portability** — `*BSD` (FreeBSD, NetBSD, OpenBSD, …) and AmigaOS 3.9+ stay compatible; shared code easy to port; no Linux-only APIs in core without guards + +## Git + +- **Identity only:** `ngteq ` — no other author name or email in this repo (`.git/config`: `user.name`, `user.email`) +- **Push only:** SSH `~/.ssh/id_ed25519_ngteq` → `git@github.com:ngteq/hyBBX.git` +- No commit/push unless the user asks diff --git a/.cursor/rules/public-product-terminology.mdc b/.cursor/rules/public-product-terminology.mdc new file mode 100644 index 0000000..5a84b52 --- /dev/null +++ b/.cursor/rules/public-product-terminology.mdc @@ -0,0 +1,31 @@ +--- +description: Public product terminology — HyBBX BBX (not BBS); never AX25SRV in shipped docs +alwaysApply: true +--- + +# Public product terminology (static rule) + +**Scope:** GitHub-facing and shipped `*.md` in this repo. Full spec: MASTER vault [AGENT-INDEX §0.16](https://github.com/ngteq/0-RESEARCHES/blob/main/AGENT-INDEX.md#016-public-product-terminology-static-rule) · `operations/2026-07-15-public-product-terminology-rule.md`. + +## HyBBX — use BBX, not BBS + +| Context | Term | +|---------|------| +| HyBBX product / UN1TME instance | **BBX** | +| Forbidden in HyBBX public copy | **BBS** | + +## Other projects — BBS allowed + +| Context | Term | +|---------|------| +| MAX25-Stack, baycom-pr, generic packet apps | **BBS** OK | +| Referring to HyBBX by name | **HyBBX** or **BBX** — not “HyBBX BBS” | + +## AX25SRV — never public + +| Scope | Rule | +|-------|------| +| Shipped docs (README, `docs/*.md`, GitHub) | **Never** `AX25SRV`, `ax25srv` in prose, titles, or filenames | +| Replace with | “dual-TNC operator flow”, `DUAL-TNC-OPERATOR-GUIDE.md`, `hybbx.un1t.me`, “UN1TME” | + +**Relation:** document-standard · terms-exclusion diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ff5bb66 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# HyBBX — https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{c,h}] +indent_style = space +indent_size = 4 + +[*.{md,ini,sh,yml,yaml}] +indent_style = space +indent_size = 4 + +[CMakeLists.txt] +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8364e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Build artifacts +build/ +build-*/ +bin/ +out/ +logs/ +data/ +local/* +!local/README.md +local/hybbx.ini +share/keys/ +shared/keys/ +share/data-proxy/ +share/data-secondary/ +cmake-build-*/ +.cursor/cache/ + +# IDE +.idea/ +.vscode/ +*.swp +compile_commands.json + +# OS +.DS_Store +Thumbs.db + +# vault/agent — never ship +AGENT-INDEX.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e9b0ab8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,65 @@ +# HyBBX — agent guide + +Humans: [CONTRIBUTING.md](CONTRIBUTING.md). **v2.0.0**. + +## Product + +Plugin-only session daemon. **Main** = users + telnet + HBX hub. **Secondary** = remote RF host (HBX client to Main). No modem DSP inside `hybbx`. + +**Text-first:** users work in plain-text terminals (`/` commands, mail, chat, line output). Core and operator docs assume text sessions on low-bandwidth links. **Extensible:** plugins and transports may add ANSI, menus, or client graphics (WebSocket/httpd) without moving wire or game logic into `src/core/`. **Portable:** POSIX+ C99 shared code — Linux, *BSD, AmigaOS 3.9+, MacOS, Windows ([PLATFORMS.md](docs/PLATFORMS.md)). + +| In tree | External | +|---------|----------| +| `src/core/`, `plugins/telnet` | TNC, KISS, sound-card apps | +| `plugins/ssh`, `plugins/websocket` | libssh; httpd for browser UI | +| Built-in `[security]` (network + abuse) | Optional external fail2ban filters in `share/fail2ban/` | +| `plugins/packet_radio` | Serial/USB TNC — RF prep: **MainAX25-Stack (MAX25)** `hardware/tncs` | +| `plugins/baycom` | BayCom modem path — prep: MAX25 `hardware/modems` (replaces standalone BayCom PR-Stack) | +| `plugins/ardop`, `plugins/crdop` | ARDOPC; CRDOPC — soft-modem prep: MAX25 `hardware/soft-modems` (replaces standalone CRDOP tree) | +| `plugins/entertain_*` (e.g. chess) | Entertain Area — games/apps on Main only | + +MAX25 owns TNC boot-wait, BayCom kernel lifecycle, and CRDOPC start; HyBBX plugins open serial/TCP **after** prep. Contract: MainAX25-Stack (MAX25) `docs/HYBBX.md`. + +## Rules + +1. Core = sessions + HBX/TCP only — no KISS/AX.25/telnet wire parsing in `src/core/` +2. **Inter-node transport:** Secondary→Main, Main↔Main (`mains_proxy`), and proxy relay (`proxymail`/`proxychat`) MUST use HBX/Circuit (`hybbx_circuit_link_connect` + `LINK_AUTH`) — never raw TCP/AX.25 sockets between HyBBX processes. User sessions (telnet/SSH/WebSocket) are separate. Proxy carries user services only — no admin actions. +3. Plugins: `hybbx_transport_plugin_t` in `plugins/`; **Entertain Area apps are plugins only** — see [ENTERTAIN.md](docs/ENTERTAIN.md) +4. Booleans: `hybbx_parse_bool()` — `yes`/`no` +5. Buffers: [limits.h](include/hybbx/limits.h) +6. **Portability:** POSIX+ friendly — C99 and portable POSIX in shared code; keep `*BSD` (FreeBSD, NetBSD, OpenBSD, …) and **AmigaOS 3.9+** compatible; isolate platform code (`_WIN32`, `__AMIGA__`, …). New core changes should stay easy to port. +7. **Logging:** `[log] level` = `debug` \| `stats` \| `info` \| `warn` (default). `hybbx_log_*` writes console + daily file when enabled. See [MANUAL.md](docs/MANUAL.md) `[log]`. +8. **Docs — technical only.** Operator/INI → [MANUAL.md](docs/MANUAL.md) + `share/*.ini.example`. Commands → `share/areas.yaml` + [commands.yaml](share/commands.yaml) + [COMMANDS.md](docs/COMMANDS.md). Build → [BUILD.md](docs/BUILD.md). No planning, roadmap, or feature-status docs. +9. **Commands:** user groups are Sysop, Admin, Mod, User, Guest only (no “Staff”). Help: two lines (`/ …` + `Help: …`); headers use `/help for more`. Output: one blank line before `/command` reply, none after (not chat/mail compose). `/broadcast` (alias `/announce`) = Sysop → all online users on **local Main** only. +10. **Proxy network** (`mains_proxy`): user services (`proxymail`, `proxychat`) only — no Sysop/Admin/Mod actions across proxy links. +11. **Version:** current `HYBBX_VERSION_STRING` only in code and brief doc headers. As-is software — compact text, no upgrade/history bloat. +12. **Documentation — Linux-based.** Operator docs in `docs/`, `text/`, README, and share examples assume Linux (`HTTPD_DOCROOT`, `systemctl`, `ss`). Platform targets belong in [PLATFORMS.md](docs/PLATFORMS.md) only — each OS in its own section (Linux, AmigaOS 3.9+, *BSD [FreeBSD, NetBSD, OpenBSD, …], **MacOS X+**, **Windows 10+**; spell **MacOS**, not macOS). Do not mix MacOS into *BSD rubrics elsewhere. +13. **Git** — no commit/push unless the user asks. **Only** `ngteq ` for author/committer (`.git/config`). Push **only** via `~/.ssh/id_ed25519_ngteq` → `git@github.com:ngteq/hyBBX.git`. + +## Doc map + +| File | Use | +|------|-----| +| [MANUAL.md](docs/MANUAL.md) | INI + operator | +| [COMMANDS.md](docs/COMMANDS.md) | Command registry, help layout | +| [commands.yaml](share/commands.yaml) | Command registry (help, aliases, access) | +| [areas.yaml](share/areas.yaml) | Menu/index area layout | +| [BUILD.md](docs/BUILD.md) | CMake | +| [DEVELOPMENT.md](docs/DEVELOPMENT.md) | Code rules | +| [TOPOLOGY.md](docs/TOPOLOGY.md) | Main, Secondary, mains-proxy; **HBX** (Hybrid Bridge eXchange v1) | +| [SECURITY.md](docs/SECURITY.md) | Security + spam policy | +| [WEBSOCKET.md](docs/WEBSOCKET.md) | WebSocket deploy | +| [MAINS_PROXY.md](docs/MAINS_PROXY.md) | Proxy network INI | +| [TNCS.md](docs/TNCS.md) | Packet radio TNC profiles | +| [BAYCOM.md](docs/BAYCOM.md) | BayCom PR-Stack | +| [ARDOP.md](docs/ARDOP.md) · [CRDOP.md](docs/CRDOP.md) | Modem plugins | +| [CLIENTS.md](docs/CLIENTS.md) | CLI clients | +| [PLATFORMS.md](docs/PLATFORMS.md) | Per-OS build notes (MacOS, Windows, …) | +| [ENTERTAIN.md](docs/ENTERTAIN.md) | Entertain Area — plugin-only apps (chess, …) | + +## Build + +```bash +./scripts/dev-setup.sh +ctest --test-dir build # -DHYBBX_BUILD_TESTS=ON +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d328409 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog · HyBBX + +All notable changes to HyBBX are documented here. + +## [HyBBX 2.4.1] + +Version `2.4.1`. Patch release. + +### Menu / help display + +| Fix | Detail | +|-----|--------| +| General area wrap | `/menu` and `/index` split long verb lists across continuation lines (80-col wire format) — fixes truncated `/users /` and missing `/session` `/version` | + +### Docs — BayCom plugin + +| Change | Detail | +|--------|--------| +| Wording | BayCom = optional transport plugin (CMake ON; example INI `baycom=no`) — removed misleading “rejected” labels | + +## [HyBBX 2.4.0] + +Version `2.4.0`. Product label **HyBBX 2.4.0**. + +### RF / MAX25 integration matrix + +| Change | Detail | +|--------|--------| +| MAX25-minimal boundary | MAX25 owns hardware prep; HyBBX probes max25d and attaches KISS only | +| `[max25] check=yes` | Local `packet_radio` **requires** reachable max25d TCP `:7325` | +| `kiss_entry=none` | Default after MAX25 prep — HyBBX does not send `kiss on` | +| BayCom plugin | Built by default; example INI templates set `[networks] baycom=no` | + +### Operator INI matrix + +| Key | v2.4.0 value | +|-----|--------------| +| `[max25] check` | `yes` | +| `kiss_entry` | `none` | +| `[networks] baycom` | `no` | + +### Carried from v2.0.0-p2 + +| Item | Notes | +|------|-------| +| YAML menu/rights registry, `hybbxd`, screen/tmux | baseline | +| TheFirmware TNC terminal recovery before KISS | baseline | +| AX.25 broadcast timing / dual-TNC sequencing | baseline | +| Circuit reconnect race fix | baseline | +| Unified log levels | baseline | + +## Related + +| Goal | Doc | +|------|-----| +| Master guide | [docs/MASTER-GUIDE.md](docs/MASTER-GUIDE.md) | +| TNC profiles | [docs/TNCS.md](docs/TNCS.md) | diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..70a6c32 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,272 @@ +cmake_minimum_required(VERSION 3.16) + +# Default C compiler when CC / CMAKE_C_COMPILER are not set: +# *BSD (FreeBSD, NetBSD, OpenBSD, …) → LLVM Clang (native toolchain on BSD) +# other → GCC +if(NOT DEFINED ENV{CC} AND NOT CMAKE_C_COMPILER) + if(CMAKE_HOST_SYSTEM_NAME MATCHES "^(FreeBSD|OpenBSD|NetBSD|DragonFly)$") + find_program(HYBBX_PREFERRED_CLANG NAMES clang) + if(HYBBX_PREFERRED_CLANG) + set(CMAKE_C_COMPILER "${HYBBX_PREFERRED_CLANG}" + CACHE FILEPATH "HyBBX C compiler (Clang on BSD)" FORCE) + else() + find_program(HYBBX_PREFERRED_GCC NAMES gcc) + if(HYBBX_PREFERRED_GCC) + set(CMAKE_C_COMPILER "${HYBBX_PREFERRED_GCC}" + CACHE FILEPATH "HyBBX C compiler (GCC fallback on BSD)" FORCE) + endif() + unset(HYBBX_PREFERRED_GCC) + endif() + unset(HYBBX_PREFERRED_CLANG) + else() + find_program(HYBBX_PREFERRED_GCC NAMES gcc) + if(HYBBX_PREFERRED_GCC) + set(CMAKE_C_COMPILER "${HYBBX_PREFERRED_GCC}" + CACHE FILEPATH "HyBBX primary C compiler (GCC)" FORCE) + endif() + unset(HYBBX_PREFERRED_GCC) + endif() +endif() + +project(HyBBX + VERSION 2.8.0 + DESCRIPTION "C99 transport service: centralized daemon + link/repeater edge" + LANGUAGES C +) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +# Export compile commands for IDE integration +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(CMakeDependentOption) + +# --------------------------------------------------------------------------- +# Options +# --------------------------------------------------------------------------- +option(HYBBX_BUILD_DAEMON "Build hybbx server daemon" ON) +option(HYBBX_BUILD_CLIENTS "Build standalone CLI clients" ON) +option(HYBBX_CLIENTS_ONLY "Clients only (no daemon, no plugins)" OFF) +option(HYBBX_BUILD_PLUGINS "Build transport plugins" ON) +option(HYBBX_PLUGIN_TELNET "Build TCP/IP telnet transport" ON) +option(HYBBX_PLUGIN_PACKET_RADIO "Build packet radio transport" ON) +option(HYBBX_PLUGIN_ARDOP "Build ARDOP host-client transport" ON) +option(HYBBX_PLUGIN_CRDOP "Build CRDOP CB host-client transport" ON) +option(HYBBX_PLUGIN_SSH "Build SSH transport (requires libssh)" ON) +option(HYBBX_PLUGIN_BAYCOM "Build BayCom/based transport plugin (default ON)" ON) +option(HYBBX_PLUGIN_MAINS_PROXY "Build Main-to-Main mesh proxy" ON) +option(HYBBX_PLUGIN_WEBSOCKET "Build WebSocket forward-proxy transport" ON) +option(HYBBX_BUILD_TESTS "Build unit tests" OFF) +option(HYBBX_HARDENING "Enable compiler security hardening" ON) +option(HYBBX_WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) +option(HYBBX_CRYPTO_OPENSSL "Optional OpenSSL crypto backends" OFF) +option(HYBBX_CRYPTO_LIBSODIUM "Optional libsodium crypto backends" OFF) +option(HYBBX_STORAGE_SQLITE "Optional SQLite storage backend" ON) + +cmake_dependent_option(HYBBX_BUILD_CLIENT_TELNET + "Build hybbx-telnet standalone client" ON "HYBBX_BUILD_CLIENTS" OFF) +cmake_dependent_option(HYBBX_BUILD_CLIENT_TERMINAL + "Build hybbx-terminal standalone client" ON "HYBBX_BUILD_CLIENTS" OFF) +cmake_dependent_option(HYBBX_BUILD_CLIENT_SSH + "Build hybbx-ssh standalone client (requires libssh)" ON "HYBBX_BUILD_CLIENTS" OFF) + +if(HYBBX_CLIENTS_ONLY) + set(HYBBX_BUILD_DAEMON OFF CACHE BOOL "Clients-only build" FORCE) + set(HYBBX_BUILD_PLUGINS OFF CACHE BOOL "Clients-only build" FORCE) + set(HYBBX_BUILD_CLIENTS ON CACHE BOOL "Clients-only build" FORCE) +endif() + +set(HYBBX_HAVE_LIBSSH FALSE) +if(HYBBX_PLUGIN_SSH OR HYBBX_BUILD_CLIENT_SSH) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(LIBSSH QUIET libssh) + endif() + if(LIBSSH_FOUND) + set(HYBBX_HAVE_LIBSSH TRUE) + message(STATUS "HyBBX: libssh ${LIBSSH_VERSION} — SSH enabled") + else() + if(HYBBX_PLUGIN_SSH) + message(WARNING "HYBBX_PLUGIN_SSH=ON but libssh was not found — SSH plugin disabled") + set(HYBBX_PLUGIN_SSH OFF CACHE BOOL "libssh not found" FORCE) + endif() + if(HYBBX_BUILD_CLIENT_SSH) + message(WARNING "HYBBX_BUILD_CLIENT_SSH=ON but libssh was not found — hybbx-ssh disabled") + set(HYBBX_BUILD_CLIENT_SSH OFF CACHE BOOL "libssh not found" FORCE) + endif() + endif() +endif() + +if(NOT HYBBX_BUILD_DAEMON) + set(HYBBX_BUILD_PLUGINS OFF CACHE BOOL "No daemon — skip plugins" FORCE) +endif() + +set(HYBBX_HAVE_OPENSSL FALSE) +if(HYBBX_CRYPTO_OPENSSL) + find_package(OpenSSL QUIET) + if(OpenSSL_FOUND) + set(HYBBX_HAVE_OPENSSL TRUE) + message(STATUS "HyBBX: OpenSSL crypto backends enabled") + else() + message(WARNING "HYBBX_CRYPTO_OPENSSL=ON but OpenSSL was not found") + endif() +endif() + +set(HYBBX_HAVE_LIBSODIUM FALSE) +if(HYBBX_CRYPTO_LIBSODIUM) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(LIBSODIUM QUIET libsodium) + endif() + if(LIBSODIUM_FOUND) + set(HYBBX_HAVE_LIBSODIUM TRUE) + message(STATUS "HyBBX: libsodium crypto backends enabled") + else() + message(WARNING "HYBBX_CRYPTO_LIBSODIUM=ON but libsodium was not found") + endif() +endif() + +set(HYBBX_HAVE_SQLITE FALSE) +if(HYBBX_STORAGE_SQLITE AND HYBBX_BUILD_DAEMON) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(SQLITE3 QUIET sqlite3) + endif() + if(SQLITE3_FOUND) + set(HYBBX_HAVE_SQLITE TRUE) + message(STATUS "HyBBX: sqlite3 ${SQLITE3_VERSION} — SQLite storage enabled") + if(SQLITE3_LIBRARY_DIRS) + link_directories(${SQLITE3_LIBRARY_DIRS}) + endif() + else() + message(WARNING "HYBBX_STORAGE_SQLITE=ON but libsqlite3 was not found") + endif() +endif() + +# --------------------------------------------------------------------------- +# Platform detection (see docs/PLATFORMS.md) +# --------------------------------------------------------------------------- +if(CMAKE_SYSTEM_NAME STREQUAL "AmigaOS") + set(HYBBX_PLATFORM_AMIGAOS TRUE) + message(STATUS "HyBBX: targeting AmigaOS 3.9+ (cross-GCC)") +elseif(WIN32) + set(HYBBX_PLATFORM_WINDOWS TRUE) + message(STATUS "HyBBX: targeting Windows 10+ (GCC/Clang via MinGW or LLVM-MinGW)") +elseif(APPLE) + set(HYBBX_PLATFORM_MACOS TRUE) + message(STATUS "HyBBX: targeting MacOS X+ (Apple Clang / LLVM Clang)") +elseif(CMAKE_SYSTEM_NAME MATCHES "Linux") + set(HYBBX_PLATFORM_LINUX TRUE) + message(STATUS "HyBBX: targeting Linux") + add_compile_definitions(_DEFAULT_SOURCE=1) +elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD|OpenBSD|NetBSD|DragonFly") + set(HYBBX_PLATFORM_BSD TRUE) + message(STATUS "HyBBX: targeting BSD (${CMAKE_SYSTEM_NAME})") +else() + message(STATUS "HyBBX: targeting ${CMAKE_SYSTEM_NAME} (POSIX/GCC/Clang)") +endif() + +if(NOT DEFINED HYBBX_PLATFORM_AMIGAOS) + set(HYBBX_PLATFORM_AMIGAOS FALSE) +endif() + +if(HYBBX_PLATFORM_AMIGAOS) + set(HYBBX_PLUGIN_SSH OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_BUILD_CLIENT_SSH OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_BUILD_CLIENT_TERMINAL OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_PLUGIN_WEBSOCKET OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_PLUGIN_BAYCOM OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_PLUGIN_MAINS_PROXY OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_PLUGIN_ARDOP OFF CACHE BOOL "AmigaOS build" FORCE) + set(HYBBX_PLUGIN_CRDOP OFF CACHE BOOL "AmigaOS build" FORCE) + message(STATUS "HyBBX: AmigaOS — hybbx-telnet client (clib2); telnet + packet_radio plugins") +endif() + +# --------------------------------------------------------------------------- +# Install layout (self-contained — all usable files in ONE tree) +# +# Default (source tree): +# cmake -B build && cmake --build build && cmake --install build +# → ${CMAKE_SOURCE_DIR}/bin/ +# +# Alternate (explicit prefix): +# cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local +# cmake --build build && sudo cmake --install build +# → /usr/local/hybbx/ +# +# cmake -B build -DCMAKE_INSTALL_PREFIX="$HOME" +# → $HOME/hybbx/ +# +# Layout under that root: +# hybbxd, hybbx-start, hybbx.ini, text/, data/, logs/, clients… +# --------------------------------------------------------------------------- + +# Default prefix = source directory → install into ./bin +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}" + CACHE PATH "HyBBX install prefix (default: source dir → ./bin)" FORCE) +endif() + +get_filename_component(_hybbx_src_abs "${CMAKE_SOURCE_DIR}" ABSOLUTE) +get_filename_component(_hybbx_pfx_abs "${CMAKE_INSTALL_PREFIX}" ABSOLUTE) +if(_hybbx_pfx_abs STREQUAL _hybbx_src_abs) + set(HYBBX_INSTALL_HOME "bin") +else() + set(HYBBX_INSTALL_HOME "hybbx") +endif() +unset(_hybbx_src_abs) +unset(_hybbx_pfx_abs) + +set(HYBBX_PLUGIN_INSTALL_DIR "${HYBBX_INSTALL_HOME}/lib/plugins") + +option(HYBBX_INSTALL_DEV + "Install headers, static libs, and CMake package files under the install tree" + OFF) + +message(STATUS "HyBBX install tree: ${CMAKE_INSTALL_PREFIX}/${HYBBX_INSTALL_HOME}" + " (HYBBX_INSTALL_DEV=${HYBBX_INSTALL_DEV})") + +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Compiler.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Hardening.cmake) + +# --------------------------------------------------------------------------- +# Subdirectories +# --------------------------------------------------------------------------- +add_subdirectory(src) + +if(HYBBX_BUILD_PLUGINS) + add_subdirectory(plugins) +endif() + +if(HYBBX_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() + +# --------------------------------------------------------------------------- +# Package config (optional — HYBBX_INSTALL_DEV=ON) +# --------------------------------------------------------------------------- +if(HYBBX_INSTALL_DEV) + include(CMakePackageConfigHelpers) + + configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/HyBBXConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/HyBBXConfig.cmake" + INSTALL_DESTINATION "${HYBBX_INSTALL_HOME}/lib/cmake/HyBBX" + ) + + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/HyBBXConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ) + + install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/HyBBXConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/HyBBXConfigVersion.cmake" + DESTINATION "${HYBBX_INSTALL_HOME}/lib/cmake/HyBBX" + ) +endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1f90ae7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing + +**v2.0.0** · standards: [AGENTS.md](AGENTS.md), [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). + +```bash +git clone && cd hyBBX +./scripts/dev-setup.sh && ./scripts/hybbx.sh +telnet 127.0.0.1 2323 +``` + +## Git identity + +**Only** this identity — no alternatives: + +``` +user.name = ngteq +user.email = info@un1t.me +``` + +Local `.git/config`. Push **only** via SSH `~/.ssh/id_ed25519_ngteq` to `git@github.com:ngteq/hyBBX.git`. + +## Pull requests + +- Small, focused diffs; Main/Secondary model unchanged unless discussed +- Behaviour / INI change → [docs/MANUAL.md](docs/MANUAL.md) + `share/*.ini.example` +- Entertain apps → plugin under `plugins/` only — [docs/ENTERTAIN.md](docs/ENTERTAIN.md) +- Verify: `cmake --build build`; telnet session if session/core touched + +Issues: `.github/ISSUE_TEMPLATE/`. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..a3b8668 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,691 @@ +Copyright (C) 2026 HyBBX contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +------------------------------------------------------------------------ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3401f04 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# HyBBX · 2.8.0 + +**hyBBX** = **hybrid Mailbox build System X**. Plannable **(hy)BBX** network system: closed / self-contained BBX core; Internet primarily for site interconnection and operator work; extensible via **plugins**. Public term: **BBX** (not BBS). + +C99 BBX daemon: text wire, `/` commands, mail, chat, conference. Main + Secondary topology over HBX; RF via external modems and MAX25 stack. + +## Feature matrix + +| Feature | Main | Secondary | Standalone | +|---------|------|-----------|------------| +| User login (telnet/SSH/WS) | yes | no | yes | +| Mail / chat / conference | yes | via Main | yes | +| HBX circuit hub | yes | client | optional | +| `packet_radio` / ARDOP / CRDOP / **BayCom/based** | via plugin | yes | yes | +| MAX25 attach (`[max25]`) | optional | typical | optional | +| `mains_proxy` | opt-in build | opt-in | opt-in | + +## Transport matrix + +| Transport | Port (default) | Notes | +|-----------|----------------|-------| +| Telnet | 2323 | Plain text | +| SSH | 3232 | Requires libssh build | +| WebSocket | configurable | Menus / clients | +| HBX | 7323 | Link auth + `link_password` | +| MAX25 KISS | via max25d :7325 | v2.8.0 attach-only | +| BayCom/based KISS | `/tmp/max25-bcpr/kiss-bc0` | via max25-bcpr · [docs/BAYCOM.md](docs/BAYCOM.md) | + +## MAX25 attach matrix + +| Rule | Value | +|------|-------| +| `kiss_entry` (local TNC) | `none` (MAX25 prep assumed) | +| `[max25] check` | `yes` (default) — start fails if max25d down | + +## Build · test · run + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build +cmake -B build-test -DHYBBX_BUILD_TESTS=ON && cmake --build build-test && ctest --test-dir build-test +./scripts/hybbx.sh +``` + +## Related + +| Goal | Doc | +|------|-----| +| Operate | [MANUAL.md](docs/MANUAL.md) | +| RF + MAX25 order | [TNCS.md](docs/TNCS.md) · [MASTER-GUIDE.md](docs/MASTER-GUIDE.md) | +| Full index | [docs/README.md](docs/README.md) | + +--- +
+ +UN1TME/UN1T.me · BBX +

+ + + + + + +
ProtocolHostPortCommand / URL
Telnethybbx.un1t.me2323telnet hybbx.un1t.me 2323
SSHhybbx.un1t.me3232ssh hybbx.un1t.me -p 3232
HTTPShybbx.un1t.me443https://hybbx.un1t.me/
HTTPhybbx.un1t.me80http://hybbx.un1t.me/
+
+hybbx.un1t.me Telnet :2323 · SSH :3232 · HTTP(S) :80/:443 · Primary RF 27.245 MHz / CB25 +
+

+ UN1TME/UN1T.me BBX — sysop menu + +
diff --git a/cmake/Compiler.cmake b/cmake/Compiler.cmake new file mode 100644 index 0000000..bd0b352 --- /dev/null +++ b/cmake/Compiler.cmake @@ -0,0 +1,97 @@ +# HyBBX toolchain detection and portable warning flags. +# +# Supported compilers: GCC (primary), LLVM Clang. +# Supported OS families: Linux, *BSD (FreeBSD, NetBSD, OpenBSD, …), MacOS X+, +# Windows 10+ (MinGW/Clang), AmigaOS 3.9+ (cross-GCC). POSIX+ friendly. + +include(CheckCCompilerFlag) + +set(HYBBX_COMPILER_GCC FALSE) +set(HYBBX_COMPILER_CLANG FALSE) +set(HYBBX_COMPILER_APPLECLANG FALSE) + +if(CMAKE_C_COMPILER_ID STREQUAL "GNU") + set(HYBBX_COMPILER_GCC TRUE) +elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang") + if(CMAKE_C_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + message(FATAL_ERROR "HyBBX requires GCC or Clang.") + endif() + set(HYBBX_COMPILER_CLANG TRUE) +elseif(CMAKE_C_COMPILER_ID STREQUAL "AppleClang") + set(HYBBX_COMPILER_CLANG TRUE) + set(HYBBX_COMPILER_APPLECLANG TRUE) +elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC") + message(FATAL_ERROR "HyBBX requires GCC or Clang.") +else() + message(WARNING + "HyBBX is developed with GCC (primary) and Clang. " + "Compiler '${CMAKE_C_COMPILER_ID}' is untested.") +endif() + +function(hybbx_probe_c_flag flag out_var) + string(REGEX REPLACE "[^a-zA-Z0-9]+" "_" flag_id "${flag}") + set(cache_var "HYBBX_C_FLAG_${flag_id}") + if(NOT DEFINED ${cache_var}) + check_c_compiler_flag("${flag}" ${cache_var}) + endif() + if(${cache_var}) + set(${out_var} TRUE PARENT_SCOPE) + else() + set(${out_var} FALSE PARENT_SCOPE) + endif() +endfunction() + +function(hybbx_collect_supported_c_flags) + set(_candidates + -Wall + -Wextra + -Wpedantic + -Wformat=2 + -Wstrict-prototypes + -Wmissing-prototypes + -Wwrite-strings + -Wshadow + -fno-strict-aliasing + ) + + if(NOT HYBBX_PLATFORM_AMIGAOS) + list(APPEND _candidates -fstack-protector-strong) + endif() + + if(HYBBX_COMPILER_GCC) + list(APPEND _candidates + -Wduplicated-cond + -Wnull-dereference + -Wlogical-op + -Wformat-signedness + ) + if(NOT CMAKE_C_COMPILER_VERSION VERSION_LESS 8.0 AND NOT HYBBX_PLATFORM_AMIGAOS) + list(APPEND _candidates -fstack-clash-protection) + endif() + endif() + + if(HYBBX_COMPILER_CLANG AND NOT HYBBX_COMPILER_APPLECLANG) + list(APPEND _candidates -Wno-gnu-zero-variadic-macro-arguments) + endif() + + set(_supported "") + foreach(flag IN LISTS _candidates) + hybbx_probe_c_flag("${flag}" _ok) + if(_ok) + list(APPEND _supported "${flag}") + endif() + endforeach() + + set(HYBBX_SUPPORTED_C_FLAGS "${_supported}" CACHE INTERNAL + "C warning/security flags supported by ${CMAKE_C_COMPILER_ID}") +endfunction() + +hybbx_collect_supported_c_flags() + +if(HYBBX_COMPILER_GCC) + message(STATUS "HyBBX compiler: GCC ${CMAKE_C_COMPILER_VERSION} (primary)") +elseif(HYBBX_COMPILER_CLANG) + message(STATUS "HyBBX compiler: Clang ${CMAKE_C_COMPILER_VERSION}") +else() + message(STATUS "HyBBX compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}") +endif() diff --git a/cmake/Hardening.cmake b/cmake/Hardening.cmake new file mode 100644 index 0000000..6bbc43f --- /dev/null +++ b/cmake/Hardening.cmake @@ -0,0 +1,44 @@ +# HyBBX compiler and linker hardening (HYBBX_HARDENING=ON) + +function(hybbx_apply_hardening target) + if(NOT HYBBX_HARDENING) + return() + endif() + + if(HYBBX_SUPPORTED_C_FLAGS) + target_compile_options(${target} PRIVATE ${HYBBX_SUPPORTED_C_FLAGS}) + endif() + + if(HYBBX_WARNINGS_AS_ERRORS) + target_compile_options(${target} PRIVATE -Werror) + endif() + + if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + if(NOT HYBBX_PLATFORM_AMIGAOS) + target_compile_definitions(${target} PRIVATE _FORTIFY_SOURCE=2) + endif() + endif() + + if(NOT HYBBX_PLATFORM_AMIGAOS AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_options(${target} PRIVATE -fPIC) + if(HYBBX_COMPILER_GCC OR HYBBX_COMPILER_CLANG) + target_link_options(${target} PRIVATE + -Wl,-z,relro + -Wl,-z,now + ) + endif() + endif() +endfunction() + +function(hybbx_apply_hardening_executable target) + hybbx_apply_hardening(${target}) + + if(NOT HYBBX_HARDENING OR HYBBX_PLATFORM_AMIGAOS) + return() + endif() + + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND (HYBBX_COMPILER_GCC OR HYBBX_COMPILER_CLANG)) + target_compile_options(${target} PRIVATE -fPIE) + target_link_options(${target} PRIVATE -pie) + endif() +endfunction() diff --git a/cmake/HyBBXConfig.cmake.in b/cmake/HyBBXConfig.cmake.in new file mode 100644 index 0000000..6c0f9ef --- /dev/null +++ b/cmake/HyBBXConfig.cmake.in @@ -0,0 +1,11 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +set(HYBBX_VERSION @PROJECT_VERSION@) + +if(NOT TARGET HyBBX::core) + include("${CMAKE_CURRENT_LIST_DIR}/HyBBXTargets.cmake") +endif() + +check_required_components(HyBBX) diff --git a/cmake/Toolchain-AmigaOS.cmake b/cmake/Toolchain-AmigaOS.cmake new file mode 100644 index 0000000..ea51fe3 --- /dev/null +++ b/cmake/Toolchain-AmigaOS.cmake @@ -0,0 +1,39 @@ +# AmigaOS 3.9+ cross-compilation toolchain (bebbo-gcc / m68k-amigaos-gcc). +# +# Usage: +# cmake -B build-amiga \ +# -DCMAKE_TOOLCHAIN_FILE=cmake/Toolchain-AmigaOS.cmake \ +# -DAMIGA_SDK_PATH=/opt/amiga +# +# AMIGA_SDK_PATH may also be set via environment variable or defaults to +# /opt/amiga when that prefix exists. See docs/PLATFORMS.md. + +set(CMAKE_SYSTEM_NAME AmigaOS) +set(CMAKE_SYSTEM_PROCESSOR m68k) + +if(NOT AMIGA_SDK_PATH) + if(DEFINED ENV{AMIGA_SDK_PATH} AND NOT "$ENV{AMIGA_SDK_PATH}" STREQUAL "") + set(AMIGA_SDK_PATH "$ENV{AMIGA_SDK_PATH}") + elseif(EXISTS "/opt/amiga/bin/m68k-amigaos-gcc") + set(AMIGA_SDK_PATH "/opt/amiga") + endif() +endif() + +if(NOT AMIGA_SDK_PATH OR NOT EXISTS "${AMIGA_SDK_PATH}/bin/m68k-amigaos-gcc") + message(FATAL_ERROR + "Set AMIGA_SDK_PATH to your AmigaOS cross-compiler prefix, e.g.\n" + " cmake -DAMIGA_SDK_PATH=/opt/amiga -DCMAKE_TOOLCHAIN_FILE=...\n" + "or export AMIGA_SDK_PATH=/opt/amiga") +endif() + +set(AMIGA_SDK_PATH "${AMIGA_SDK_PATH}" CACHE PATH + "AmigaOS cross-compiler prefix (bebbo-gcc)" FORCE) + +set(CMAKE_C_COMPILER "${AMIGA_SDK_PATH}/bin/m68k-amigaos-gcc") +set(CMAKE_AR "${AMIGA_SDK_PATH}/bin/m68k-amigaos-ar") +set(CMAKE_RANLIB "${AMIGA_SDK_PATH}/bin/m68k-amigaos-ranlib") + +set(CMAKE_FIND_ROOT_PATH "${AMIGA_SDK_PATH}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/cmake/Toolchain-Clang.cmake b/cmake/Toolchain-Clang.cmake new file mode 100644 index 0000000..d263d2d --- /dev/null +++ b/cmake/Toolchain-Clang.cmake @@ -0,0 +1,6 @@ +# Optional LLVM/Clang toolchain file. +# +# Usage: +# cmake -B build-clang -DCMAKE_TOOLCHAIN_FILE=cmake/Toolchain-Clang.cmake + +set(CMAKE_C_COMPILER clang CACHE FILEPATH "C compiler") diff --git a/docs/ARDOP.md b/docs/ARDOP.md new file mode 100644 index 0000000..8196523 --- /dev/null +++ b/docs/ARDOP.md @@ -0,0 +1,27 @@ +# ARDOP plugin · HyBBX 2.8.0 + +HyBBX transport plugin for ARDOP modem paths — external modem ownership. + +## Status matrix + +| Item | Value | +|------|-------| +| CMake | `HYBBX_PLUGIN_ARDOP=ON` (default) | +| INI | `[networks] ardop=yes` when modem available | +| RF prep | External ARDOP stack — not MAX25 CRDOP | +| Host role | Secondary typical | + +## Config matrix + +| Item | Value | +|------|-------| +| Transport section | `[transport.ardopN]` per instance | +| Circuit link | `circuit_host`, `link_id`, `link_password` | +| Protocol | ARDOP session bridge to HBX | + +## Related + +| Goal | Doc | +|------|-----| +| Topology | [TOPOLOGY.md](TOPOLOGY.md) | +| CRDOP (soft modem) | [CRDOP.md](CRDOP.md) | diff --git a/docs/BAYCOM.md b/docs/BAYCOM.md new file mode 100644 index 0000000..14ef442 --- /dev/null +++ b/docs/BAYCOM.md @@ -0,0 +1,57 @@ +# BayCom/based · HyBBX + +Public mark: **BayCom/based**. Never **Konverter** / **converter**. + +**Transport plugin** (`plugins/baycom/`) for PC-COM / SER12-class hardware via MAX25. SER12 L1 (HDLC + UART modem-control) lives in **MAX25 max25-bcpr** (device **`max25e0`**) — not inside HyBBX `packet_radio`. + +## Status: available and usable + +| Item | Value | +|------|-------| +| Build | `HYBBX_PLUGIN_BAYCOM=ON` (**default**) | +| Enable | `[networks] baycom=yes` + `[transport.baycom1]` | +| Standalone Main | `[instance] standalone=yes` — local RF with TNC + BayCom on one host | +| KISS device | **`/tmp/max25-bcpr/kiss-bc0`** (max25d releases PTY after prep) | +| Start order | **max25d first** → `[max25] check=yes` → hybbxd | +| INI examples | `share/hybbx-standalone.ini.example` · `share/hybbx/baycom-ser12-host.ini.example` | + +`packet_radio` rejects `tnc=baycom` / `tnc=pccom` (not TNC profiles). Use the **baycom** transport for PC-COM. + +## Minimal INI + +```ini +[instance] +standalone = yes + +[networks] +ax25 = yes +baycom = yes + +[transport.baycom1] +backend = kiss +device = /tmp/max25-bcpr/kiss-bc0 +serial_baud = 1200 +frequency_mhz = 26.675 + +[max25] +check = yes +host = 127.0.0.1 +port = 7325 +``` + +## Operator notes + +| Rule | Detail | +|------|--------| +| RX before TX | Prove decode / Soft-DCD before live TX on PC-COM | +| Circuit | BayCom link connects to internal HBX hub (`circuit_host` / `circuit_port`) | +| Broadcast | AX.25 auto-beacon includes baycom link when circuit QoS qualifies | + +## Related + +| Goal | Doc | +|------|-----| +| TNC serial (`packet_radio`) | [TNCS.md](TNCS.md) | +| Manual | [MANUAL.md](MANUAL.md) | +| MAX25 BayCom/based | MAX25-Stack [docs/BAYCOM.md](../MAX25-Stack/docs/BAYCOM.md) · device `max25e0` | +| Co-host runbook | [MASTER-GUIDE.md](MASTER-GUIDE.md) | diff --git a/docs/BUILD.md b/docs/BUILD.md new file mode 100644 index 0000000..52d6592 --- /dev/null +++ b/docs/BUILD.md @@ -0,0 +1,44 @@ +# Build · HyBBX 2.8.0 + +POSIX+ build reference. Platforms: [PLATFORMS.md](PLATFORMS.md). + +## Build matrix + +| Step | Command | +|------|---------| +| Release build | `cmake -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build` | +| Run | `./scripts/hybbx.sh` | +| Test | `-DHYBBX_BUILD_TESTS=ON` → `ctest --test-dir build` | +| Telnet smoke | `telnet 127.0.0.1 2323` | +| SSH smoke | `ssh 127.0.0.1 -p 3232` | + +## CMake options matrix + +| Option | Default | Description | +|--------|---------|-------------| +| `HYBBX_BUILD_DAEMON` | ON | `hybbxd` + core | +| `HYBBX_BUILD_CLIENTS` | ON | CLI clients | +| `HYBBX_PLUGIN_PACKET_RADIO` | ON | Packet radio | +| `HYBBX_PLUGIN_BAYCOM` | ON | BayCom plugin (v2.8.0 built; runtime off) | +| `HYBBX_PLUGIN_MAINS_PROXY` | OFF | Main-to-Main mesh proxy | +| `HYBBX_PLUGIN_ARDOP` | ON | ARDOP plugin | +| `HYBBX_PLUGIN_CRDOP` | ON | CRDOP plugin | +| `HYBBX_PLUGIN_SSH` | ON | SSH (libssh) | +| `HYBBX_PLUGIN_WEBSOCKET` | ON | WebSocket proxy | +| `HYBBX_BUILD_TESTS` | OFF | Unit tests | +| `HYBBX_STORAGE_SQLITE` | ON | SQLite backend | + +## Config matrix + +| Item | Value | +|------|-------| +| Live INI | `./local/hybbx.ini` (gitignored) | +| Templates | `share/hybbx-*.ini.example` | +| Secrets | `./local/` only — never commit | + +## Related + +| Goal | Doc | +|------|-----| +| Platforms | [PLATFORMS.md](PLATFORMS.md) | +| Developer | [DEVELOPMENT.md](DEVELOPMENT.md) | diff --git a/docs/CLIENTS.md b/docs/CLIENTS.md new file mode 100644 index 0000000..476afc6 --- /dev/null +++ b/docs/CLIENTS.md @@ -0,0 +1,28 @@ +# Clients · HyBBX 2.8.0 + +User-facing connection clients for HyBBX sessions. + +## Client matrix + +| Client | Transport | Default port | +|--------|-----------|--------------| +| `hybbx-telnet` | TCP telnet | 2323 | +| `hybbx-ssh` | SSH (libssh) | 3232 | +| `hybbx-terminal` | HBX circuit | 7323 | +| Web browser | WebSocket via reverse proxy | site-specific | + +## Session matrix + +| Item | Value | +|------|-------| +| Wire format | Plain text lines + `/` commands | +| ANSI | `[traffic] ansi=yes` optional | +| Guest login | `[service] auto_login=yes` | +| Registered users | `/login` after connect | + +## Related + +| Goal | Doc | +|------|-----| +| WebSocket setup | [WEBSOCKET.md](WEBSOCKET.md) | +| Manual | [MANUAL.md](MANUAL.md) | diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md new file mode 100644 index 0000000..bb664e7 --- /dev/null +++ b/docs/COMMANDS.md @@ -0,0 +1,36 @@ +# Commands · HyBBX 2.8.0 + +Slash-command reference — five access levels: Sysop → Admin → Mod → User → Guest. + +## Level matrix + +| Level | Typical commands | +|-------|------------------| +| Sysop | `/broadcast`, `/broadcast ax25`, `/shutdown` | +| Admin | `/usercreate`, `/activate`, `/promote`, `/demote` | +| Mod | moderation subset | +| User | `/mail`, `/chat`, `/who` | +| Guest | `/help`, `/menu` (filtered) | + +## RF command matrix + +| Command | Scope | Level | +|---------|-------|-------| +| `/broadcast ` | Local online users | Sysop | +| `/broadcast ax25` | Sequential RF beacon | Sysop | + +## Registry matrix + +| Item | Path | +|------|------| +| Command definitions | `share/commands.yaml` | +| Area definitions | `share/areas.yaml` | +| `/index` | Lists all commands (every account) | +| `/who` (v2.8.0) | Interactive users only — not RF sessions | + +## Related + +| Goal | Doc | +|------|-----| +| Manual | [MANUAL.md](MANUAL.md) | +| Security | [SECURITY.md](SECURITY.md) | diff --git a/docs/CRDOP.md b/docs/CRDOP.md new file mode 100644 index 0000000..0eb333e --- /dev/null +++ b/docs/CRDOP.md @@ -0,0 +1,35 @@ +# CRDOP plugin · HyBBX 2.8.0 + +HyBBX transport for MAX25-SoftModem (CRDOP) — acoustic AX.25 over soundcard. + +## Integration matrix + +| Layer | Owner | +|-------|-------| +| Soft-modem prep, TCP 8515/8516 | MAX25 `soft-crdop` | +| HyBBX transport | `crdop` plugin | +| INI merge | `share/hybbx/crdop-host.ini.example` | + +## Config matrix + +| Item | Value | +|------|-------| +| Enable | `[networks] crdop=yes` | +| CMake | `HYBBX_PLUGIN_CRDOP=ON` (default) | +| `modem_host` | MAX25 host (loopback or LAN) | +| `modem_port` | `8515` (control) | + +## Start order matrix + +| # | Action | +|---|--------| +| 1 | `max25-ctl start --hardware soft-modems --device soft-crdop` | +| 2 | Merge CRDOP host INI fragment | +| 3 | Start HyBBX with `[networks] crdop=yes` | + +## Related + +| Goal | Doc | +|------|-----| +| MAX25 CRDOP | MAX25-Stack `docs/CRDOP.md` | +| Manual | [MANUAL.md](MANUAL.md) | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..c7a8182 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,37 @@ +# Development · HyBBX 2.8.0 + +Contributor build, test, and code layout reference. + +## Workflow matrix + +| Step | Command | +|------|---------| +| Debug build | `cmake -B build -DCMAKE_BUILD_TYPE=Debug && cmake --build build` | +| Tests | `-DHYBBX_BUILD_TESTS=ON && ctest --test-dir build` | +| Clients only | `-DHYBBX_CLIENTS_ONLY=ON` | +| Format / lint | project scripts in `scripts/` | + +## Layout matrix + +| Path | Content | +|------|---------| +| `src/` | Daemon core | +| `plugins/` | Transport plugins | +| `include/hybbx/` | Public headers | +| `share/` | INI templates, YAML registries | +| `tests/` | Unit tests (opt-in build) | + +## v2.8.0 dev notes matrix + +| Topic | Rule | +|-------|------| +| MAX25 boundary | No BayCom/PC-COM in `packet_radio` | +| Local TNC tests | Mock max25d `:7325` recommended | +| Live secrets | `./local/` only | + +## Related + +| Goal | Doc | +|------|-----| +| Build | [BUILD.md](BUILD.md) | +| Contributing | [../CONTRIBUTING.md](../CONTRIBUTING.md) | diff --git a/docs/DUAL-TNC-OPERATOR-GUIDE.md b/docs/DUAL-TNC-OPERATOR-GUIDE.md new file mode 100644 index 0000000..421a183 --- /dev/null +++ b/docs/DUAL-TNC-OPERATOR-GUIDE.md @@ -0,0 +1,179 @@ + + + +# Dual-TNC operator guide · HyBBX + +**Shipped guide** — canonical edits in research vault `projects/integration/2026-07-13-master-operator-guide.md`; run sync script after change. + +## Summary + +Linear operator sequence for a **standalone Main** host with **one or two local TNCs** (dual-TNC pattern): MAX25 prep first, HyBBX second, RF verification last. Applies to any dual-TNC reference station — adjust device paths and INI per site. + +--- + +## 1. Scope + +| In scope | Out of scope | +|----------|--------------| +| Single-host standalone Main + local `packet_radio` | Remote Secondary + Main split (see HyBBX TOPOLOGY) | +| TNC2C / PK-TNC2 class (TheFirmware) | BayCom-only or CRDOP-only sites (see MAX25 master) | +| MAX25 prep → HyBBX → `/broadcast ax25` | mains_proxy mesh | + +Boundary SSoT: [2026-07-12-max25-hybbx-boundary-final.md](2026-07-12-max25-hybbx-boundary-final.md). + +--- + +## 2. Responsibility split + +| Phase | MAX25 | HyBBX | +|-------|-------|-------| +| TNC cold boot (DTR+RTS at power-on) | `tnc2c-boot-wait.sh` / max25d | — | +| Host MYCALL, TXDELAY, `kiss on` | max25d or boot-wait | — | +| Serial port ownership in operation | max25d (optional) **or** HyBBX after prep | Opens `/dev/ttyS*` after prep | +| max25d TCP probe | Listens `:7325` | `[max25] check=yes` | +| KISS DATA / UI frames / PTT path | — | `packet_radio` plugin | +| User sessions, `/broadcast ax25` | — | Main + HBX loopback | + +**One process per `/dev/tty*`** — never minicom + boot-wait + HyBBX concurrently. + +--- + +## 3. Pre-flight checklist + +| # | Check | Pass criterion | +|---|-------|----------------| +| 1 | MAX25 scripts installed | `MAX25-Stack/stacks/tncs/tnc2c-boot-wait.sh` executable | +| 2 | HyBBX built with `packet_radio` | `hybbxd` starts, plugin loads | +| 3 | INI merged | Vault template: `share/` INI examples | +| 4 | `[broadcast] ax25_mycall` set | Valid callsign for on-air source | +| 5 | `kiss_entry=none` on each `[transport.packet_radioN]` | MAX25 owns KISS entry | +| 6 | Unique `link_id` per transport | Matches circuit bridge registry | +| 7 | Radio wired, antenna safe | Bench or low-power test first | +| 8 | Radio squelch profile | **Primary:** SQ **on** (manual/ASQ) while local DCD/decode OK; open SQ only if needed; fringe/DX → Secondary or second Main — RFG/FMQ **off**; SSoT: [../../hardware/radio-equipment/2026-07-14-packet-radio-squelch-rfg-profiles.md](../../hardware/radio-equipment/2026-07-14-packet-radio-squelch-rfg-profiles.md) | + +Extended checklist: [2026-07-12-max25-tnc-prep-check.md](2026-07-12-max25-tnc-prep-check.md). + +--- + +## 4. Linear start sequence + +### Step A — Stop conflicting owners + +```bash +pkill hybbxd || true +# ensure no minicom/screen on /dev/ttyS4 / ttyS5 +``` + +### Step B — MAX25 TNC prep (per port) + +For each TNC (example ports — replace per site): + +```bash +cd /path/to/MAX25-Stack/stacks/tncs + +# TNC unit A (cold boot or stuck host) +./tnc2c-boot-wait.sh --device /dev/ttyS4 --recover-only +# or full boot-wait with power cycle if DTR was low at cold start + +# TNC unit B (PK-TNC2) +./pktnc2-boot-wait.sh --device /dev/ttyS5 --recover-only +``` + +**Optional:** start `max25d` instead of one-shot scripts — enables serial watch and M25/1: + +```bash +max25-ctl start --hardware tncs --device tnc2c +ss -ltn | grep 7325 # expect LISTEN on 7325 +``` + +Note: one `max25d` instance = one RF backend in v1; dual-TNC sites often use **scripts per port** then HyBBX as serial owner. See vault prep check for dual-TNC pattern. + +### Step C — Verify MAX25 reachability + +```bash +nc -zv 127.0.0.1 7325 # when max25d running +# or: confirm boot-wait log shows TheFirmware banner + KISS ready +``` + +### Step D — Start HyBBX + +```bash +hybbxd -c /path/to/hybbx.ini +# or: hybbx-start +``` + +**Expected log sequence:** + +1. `max25d reachable` (if `[max25] check=yes`) +2. `KISS attach (MAX25 prep assumed)` per `[transport.packet_radioN]` +3. Circuit hub listening `:7323` + +### Step E — RF verification + +| Test | Command / action | Pass | +|------|------------------|------| +| Local user session | `hybbx-telnet -H 127.0.0.1` | Login OK | +| Manual RF beacon | `/broadcast ax25` (Sysop) | Log `RF TX`; PTT at radio | +| On-air decode | Remote monitor or second radio | AX.25 UI frame with `ax25_mycall` | +| Dual link gap | Two transports configured | ~60 s between links (sequential) | + +RF debug timeline: [../hybbx/2026-07-12-site-rf-broadcast-investigation.md](../hybbx/2026-07-12-site-rf-broadcast-investigation.md). + +--- + +## 5. INI pointers (dual local TNC) + +Site INI: merge `share/` examples per deployment. + +| Section | Keys to verify | +|---------|----------------| +| `[networks]` | `circuit=yes`, `ax25=yes` | +| `[transport.circuit]` | `port=7323`, `max_links` ≥ transport count | +| `[transport.packet_radio1]` | `device`, `tnc`, `link_id`, `link_password`, `kiss_entry=none`, `persist=255` | +| `[transport.packet_radio2]` | same pattern — different `device`, `link_id` | +| `[broadcast]` | `ax25_mycall`, `ax25_auto_message`, `ax25_dest` | +| `[max25]` | `check=yes`, `host=127.0.0.1`, `port=7325` | + +HyBBX shipped templates: `hyBBX/share/hybbx-standalone.ini.example`. + +--- + +## 6. Recovery without full host reboot + +| Symptom | Action | +|---------|--------| +| Echo-only on one TNC | Stop HyBBX → `--recover-only` on that port → restart HyBBX | +| max25d `error-host` | Serial watch auto-repair; or manual `tnc2c-host-reset.sh --kiss` | +| No PTT, log shows `RF TX` | Check FCS strip, `persist=255`, MYCALL prep — vault RF investigation | +| Stuck after HyBBX crash | KISS return frame before re-prep; do not skip MAX25 step | + +Runbook: [operations/runbooks/tnc-recovery-without-power-cycle.md](../../operations/runbooks/tnc-recovery-without-power-cycle.md). + +--- + +## 7. Reboot order (reference station) + +After host power cycle: + +1. **Physical:** TNC power with DTR high (boot-wait script running **before** or **during** TNC power-on) +2. **MAX25:** boot-wait per TNC (mandatory for TNC2C cold boot) +3. **HyBBX:** start immediately after successful prep (HyBBX re-asserts RTS/DTR) +4. **Verify:** `/broadcast ax25` within 5 min of uptime + +Do **not** start HyBBX before TNC host mode is confirmed — HyBBX cannot recover Landolt cold-boot without DTR sequencing. + +--- + +## Related + +| Topic | Path | +|-------|------| +| MAX25 ↔ HyBBX boundary | [2026-07-12-max25-hybbx-boundary-final.md](2026-07-12-max25-hybbx-boundary-final.md) | +| TNC prep checklist | [2026-07-12-max25-tnc-prep-check.md](2026-07-12-max25-tnc-prep-check.md) | +| HyBBX master guide | [../hybbx/2026-07-13-master-documentation.md](../hybbx/2026-07-13-master-documentation.md) | +| MAX25 master guide | [../max25-stack/2026-07-13-master-documentation.md](../max25-stack/2026-07-13-master-documentation.md) | +| RF investigation | [../hybbx/2026-07-12-site-rf-broadcast-investigation.md](../hybbx/2026-07-12-site-rf-broadcast-investigation.md) | +| Production INI | `share/` INI examples | +| TNC recovery runbook | [operations/runbooks/tnc-recovery-without-power-cycle.md](../../operations/runbooks/tnc-recovery-without-power-cycle.md) | +| TNC2multi 1200/9600 (Amateurfunk) | [hardware/tnc2multi/2026-07-14-tnc2multi-modulation-1200-9600.md](../../hardware/tnc2multi/2026-07-14-tnc2multi-modulation-1200-9600.md) | +| Packet Squelch/RFG (Primary/Secondary) | [hardware/radio-equipment/2026-07-14-packet-radio-squelch-rfg-profiles.md](../../hardware/radio-equipment/2026-07-14-packet-radio-squelch-rfg-profiles.md) | diff --git a/docs/ENTERTAIN.md b/docs/ENTERTAIN.md new file mode 100644 index 0000000..cbdec78 --- /dev/null +++ b/docs/ENTERTAIN.md @@ -0,0 +1,25 @@ +# Entertain Area · HyBBX 2.8.0 + +Optional Main-only plugins for games and apps — not on Secondary RF hosts. + +## Plugin matrix + +| Plugin | Status | Notes | +|--------|--------|-------| +| Entertain Area framework | opt-in CMake | Main hosts only | +| Chess / games | deferred | future or doc removal | + +## Config matrix + +| Item | Value | +|------|-------| +| Enable | `-DHYBBX_PLUGIN_ENTERTAIN=ON` (when available) | +| Host role | Main only — Secondary runs RF transports | +| User access | Level-gated via `[areas]` / `share/areas.yaml` | + +## Related + +| Goal | Doc | +|------|-----| +| Topology | [TOPOLOGY.md](TOPOLOGY.md) | +| Commands | [COMMANDS.md](COMMANDS.md) | diff --git a/docs/LICENSING.md b/docs/LICENSING.md new file mode 100644 index 0000000..662505f --- /dev/null +++ b/docs/LICENSING.md @@ -0,0 +1,26 @@ +# Licensing · HyBBX 2.8.0 + +HyBBX and shipped components — license reference. + +## License matrix + +| Component | License | +|-----------|---------| +| HyBBX core + plugins | GPL-3.0 — [LICENSE.txt](../LICENSE.txt) | +| Bundled crypto (tinysha256, tinyaes, monocypher) | respective upstream licenses | +| Optional OpenSSL / libsodium | upstream licenses when enabled | + +## Distribution matrix + +| Item | Rule | +|------|------| +| Source offer | GPL-3.0 compliance required for derivatives | +| Operator config | `./local/` — not part of distribution | +| Third-party deps | Document in build output / package metadata | + +## Related + +| Goal | Doc | +|------|-----| +| Build options | [BUILD.md](BUILD.md) | +| Root README | [../README.md](../README.md) | diff --git a/docs/MAINS_PROXY.md b/docs/MAINS_PROXY.md new file mode 100644 index 0000000..7573cbe --- /dev/null +++ b/docs/MAINS_PROXY.md @@ -0,0 +1,37 @@ +# mains_proxy · HyBBX 2.8.0 + +Opt-in mesh linking Main or Secondary instances for `/proxymail` and `/proxychat`. + +## Feature matrix + +| Feature | Supported | +|---------|-----------| +| `/proxymail` cross-site | yes | +| `/proxychat` cross-site | yes | +| User account sync | no | +| Sysop actions over proxy | no | +| Raw Main-to-Main TCP bypass | no — HBX/Circuit only | + +## Build matrix + +| Item | Value | +|------|-------| +| CMake | `-DHYBBX_PLUGIN_MAINS_PROXY=ON` | +| INI | `[networks] mains_proxy=yes` | +| Template | `share/hybbx-mesh.ini.example` | +| Auth | Same as Secondary — `link_id`, `link_password` | + +## Topology matrix + +| Peer A | Peer B | Transport | +|--------|--------|-----------| +| Main | Main | HBX circuit client | +| Main | Secondary | HBX circuit client | +| Secondary | Secondary | via Main hub (typical) | + +## Related + +| Goal | Doc | +|------|-----| +| Topology | [TOPOLOGY.md](TOPOLOGY.md) | +| Manual | [MANUAL.md](MANUAL.md) | diff --git a/docs/MANUAL.md b/docs/MANUAL.md new file mode 100644 index 0000000..699e638 --- /dev/null +++ b/docs/MANUAL.md @@ -0,0 +1,63 @@ +# Operator manual · HyBBX 2.8.0 + +Telnet, SSH, WebSocket operator reference. INI templates in `share/` — copy to `./local/hybbx.ini`. + +## Template matrix + +| Template | Use | +|----------|-----| +| `hybbx-standalone.ini.example` | Main + local TNC (one host) | +| `hybbx-main.ini.example` | Main; RF on remote Secondary | +| `hybbx-mesh.ini.example` | Main + `mains_proxy` mesh | +| `hybbx-secondary.ini.example` | Secondary RF host | + +## Topology matrix + +| Role | `[networks]` typical | Hosts | +|------|----------------------|-------| +| **Main** | `circuit=yes`, `ax25=no` | Users, HBX hub | +| **Secondary** | `circuit=no`, `ax25=yes` | TNC/RF; `circuit_host` → Main | +| **Standalone Main** | `ax25=yes` on same box | Lab / single-host | + +## Core INI sections matrix + +| Section | Key keys | +|---------|----------| +| `[service]` | `name`, `max_online`, `prompt` | +| `[storage]` | `backend=flatfile\|sqlite` | +| `[networks]` | `ax25`, `baycom`, `crdop`, `circuit`, `ssh`, `websocket` | +| `[transport.telnet]` | `bind`, `port=2323` | +| `[transport.circuit]` | `port=7323`, `max_links` | +| `[transport.packet_radioN]` | `device`, `tnc`, `protocol=kiss`, `kiss_entry=none` | +| `[broadcast]` | `ax25_mycall`, `ax25_auto_message`, `ax25_dest` | +| `[max25]` | `check=yes`, `host`, `port=7325` | +| `[security]` | `bantime`, `maxretry`, `ban_backend` | + +## v2.8.0 RF keys matrix + +| Key | Value | Notes | +|-----|-------|-------| +| `kiss_entry` | `none` | MAX25 owns KISS entry | +| `kiss_exit` | `none` | No `kiss off` on shutdown | +| `[max25] check` | `yes` | Local TNC start fails if max25d down | + +## BayCom/based (via MAX25) + +Built by default. Enable `[networks] baycom=yes`; KISS device **`/tmp/max25-bcpr/kiss-bc0`**. Standalone template: `share/hybbx-standalone.ini.example`. Start max25d before HyBBX; prove RX before live TX. Full detail: [BAYCOM.md](BAYCOM.md). + +## Daemon matrix + +| Item | Value | +|------|-------| +| Binary | `hybbxd` | +| Start | `hybbx-start` or `hybbxd -c hybbx.ini` | +| Detached | `hybbx-start --screen` / `--tmux` | +| First start | Sysop created with random password on console | + +## Related + +| Goal | Doc | +|------|-----| +| Topology | [TOPOLOGY.md](TOPOLOGY.md) | +| Security | [SECURITY.md](SECURITY.md) | +| Build | [BUILD.md](BUILD.md) | diff --git a/docs/MASTER-GUIDE.md b/docs/MASTER-GUIDE.md new file mode 100644 index 0000000..53895af --- /dev/null +++ b/docs/MASTER-GUIDE.md @@ -0,0 +1,211 @@ + + + +# HyBBX — Master operator guide + +**Shipped guide** — canonical edits in research vault `projects/hybbx/2026-07-13-master-documentation.md`; run sync script after change. + +## Summary + +Single linear guide: what HyBBX is, how to install and configure it, attach RF via MAX25 prep, run clients and commands. Shipped product docs in `docs/` remain **frozen reference** — depth investigations live. + +--- + +## 1. Role and architecture + +HyBBX (**hyBBX** — hybrid Mailbox build System X) is a **plannable (hy)BBX network system**: closed and self-contained BBX networking; the Internet is used primarily for interconnection and logical work; Internet and external services are fully extensible via **plugins** ([§0.17](../../AGENT-INDEX.md#017-hybbx-product-positioning-static-rule)). + +Technically: a **text-first BBX/mail/chat** daemon (`hybbxd`) with optional RF transports. Users connect via telnet (`:2323`), SSH (`:3232`), or WebSocket proxy. RF paths use transport plugins that bridge serial/KISS or kernel modems to **HBX** (Hybrid Bridge eXchange) on the circuit hub (`:7323`). + +``` +Users (telnet/SSH/WebSocket) ──► Main (storage, mail, HBX hub :7323) + ▲ + │ HBX/TCP + Secondary (packet_radio / baycom / crdop) + │ + TNC / modem (after MAX25 prep) +``` + +| Role | `[networks]` | Hosts | +|------|--------------|-------| +| **Main** | `circuit=yes`, `ax25=no` (typical) | Users, HBX hub | +| **Secondary** | `circuit=no`, `ax25=yes` | RF near TNC; `circuit_host` → Main | +| **Standalone Main** | `ax25=yes` on same box | Lab / single-host (e.g. dual local TNC) | + +**HBX/Circuit** — sole inter-node transport. Application core never sees raw KISS or on-air AX.25; only typed HBX frames on TCP. + +Frozen reference: `docs/TOPOLOGY.md`, `docs/MANUAL.md`. + +--- + +## 2. Install and build + +| Step | Action | +|------|--------| +| Build | `hyBBX/scripts/build.sh` or CMake per `docs/BUILD.md` | +| Binary | `hybbxd` — start via `hybbx-start` or `hybbxd -c hybbx.ini` | +| Detached | `hybbx-start --screen` / `--tmux`; attach: `hybbxd --screen --attach` | +| INI templates | `hyBBX/share/hybbx-standalone.ini.example`, `hybbx-main.ini.example`, `hybbx-secondary.ini.example`, `hybbx-mesh.ini.example` | + +First start creates Sysop in `users/users.ini` with a one-time random password on the console. + +--- + +## 3. INI — operator essentials + +Full key tables: frozen `docs/MANUAL.md`. Vault INI copy (reference station): `share/` INI examples. + +### Core sections + +| Section | Purpose | +|---------|---------| +| `[service]` | Name, session limit, prompt | +| `[storage]` | `flatfile` or `sqlite`; user shards under `users/` | +| `[networks]` | Enable plugins: `ax25`, `baycom`, `crdop`, `circuit`, `ssh`, `websocket` | +| `[transport.telnet]` | Bind, port `2323` | +| `[transport.circuit]` | HBX hub port `7323`, `max_links` (default 8, max 16) | +| `[transport.packet_radioN]` | TNC instance — device, `tnc=` profile, `protocol=kiss` | +| `[broadcast]` | `ax25_mycall`, `ax25_auto_message`, `ax25_dest` | +| `[max25]` | `check=yes` — probe max25d TCP `:7325` before local serial open | + +### RF on Main vs Secondary + +| Layout | TNC keys on Main `[transport.packet_radioN]` | +|--------|-----------------------------------------------| +| Remote Secondary | Bridge registry only (`link_id`, password) — no `device` | +| Local TNC (standalone) | Full keys: `device`, `tnc`, `baud`, `serial_line`, `rts_dtr` | + +Bridge-registry rows without `device` are skipped at start (no serial open). + +--- + +## 4. TNC attach (MAX25 contract) — v2.8.0 + +**Order:** MAX25 prep **before** HyBBX for local serial TNC. One process per `/dev/tty*`. HyBBX **attach-only** on KISS (`kiss_entry=none` after max25d prep). + +| Layer | Owner | Action | +|-------|-------|--------| +| Boot-wait, DTR/RTS, MYCALL, `kiss on` | MAX25 | `max25-ctl start --hardware tncs` or `tnc2c-boot-wait.sh` | +| max25d reachability | HyBBX `[max25] check=yes` | TCP `:7325` — **required** for local TNC (start fails if down) | +| KISS attach | HyBBX `packet_radio` | `kiss_entry=none` (default with MAX25) | +| AX.25 UI build, HBX bridge, broadcast | HyBBX | `broadcast.c`, `packet_radio.c`, `tnc.c` | +| BayCom/based plugin | HyBBX | Built by default; `[networks] baycom=yes` when RF used — [operational status](../integration/2026-08-01-baycom-pccom-operational-status.md) | + +HyBBX builds outbound UI frames from `[broadcast] ax25_mycall` — it does **not** send host `MYCALL` to the TNC (MAX25 does that at prep). + +| INI key | Production value | Notes | +|---------|------------------|-------| +| `kiss_entry` | `none` | TNC already in KISS after MAX25 | +| `kiss_exit` | `none` | Shutdown does not send `kiss off` | +| `persist` | `255` on CB | CSMA — avoid random defer on busy channel | +| `protocol` | `kiss` | Required for Secondary RF | +| `[max25] check` | `yes` | Local serial edges only | + +TNC profiles in `packet_radio`: `tnc2c`, `tnc2`, `pk232`, … — see shipped `docs/TNCS.md`. BayCom/based transport: **built by default**, **available and usable** — enable `[networks] baycom=yes`; KISS **`/tmp/max25-bcpr/kiss-bc0`**; standalone/co-host verified 2026-07-31 / 2026-08-01. + +Release prep: [2026-07-14-v2.8.0-release-prep.md](2026-07-14-v2.8.0-release-prep.md). + +Recovery without power cycle: internal research note) · MAX25 `stacks/tncs/docs/TNC-RECOVERY.md`. + +--- + +## 5. RF, broadcast, and HBX + +### Auto-beacon and manual broadcast + +| Command | Scope | Sysop | +|---------|-------|-------| +| `/broadcast ` | Local logged-in users (telnet/SSH/WebSocket) | yes | +| `/broadcast ax25` | Sequential RF beacon per link (`ax25_auto_message`; 60 s gap) | yes | + +RF path: Main → HBX → Secondary link → KISS → TNC → on-air. + +### Standalone Main production facts + +Dual local TNC + optional BayCom on one host — shipped: [BAYCOM.md](BAYCOM.md) · [DUAL-TNC-OPERATOR-GUIDE.md](DUAL-TNC-OPERATOR-GUIDE.md). Vault depth: [co-host runbook](../integration/2026-07-31-standalone-cohost-runbook.md) · [BayCom operational status](../integration/2026-08-01-baycom-pccom-operational-status.md). + +| Issue class | Vault SSoT fix | +|-------------|----------------| +| No PTT despite `RF TX` log | FCS strip, MYCALL before KISS, UNPROTO path, `persist=255` | +| Circuit queue dropped beacons | Bypass low-prio queue for AX.25 broadcast TX | +| `ax25_dest=*` invalid | Map to `QST` | +| TNC stuck after crash | KISS return frame `0xC0 0xFF 0xC0` before prep | + +### BayCom and CRDOP plugins + +| MAX25 hardware | HyBBX plugin | INI | +|----------------|--------------|-----| +| `hardware/modems` | `baycom` | `[networks] baycom=yes` — merge `share/hybbx/baycom-ser12-host.ini.example` | +| **BayCom/based** (max25-bcpr via MAX25) | available · default build | Standalone Main or co-host (TNC + PC-COM); KISS `/tmp/max25-bcpr/kiss-bc0` — [plugin](2026-07-18-baycom-based-plugin-via-max25.md) · [operational status](../integration/2026-08-01-baycom-pccom-operational-status.md) | +| `hardware/soft-modems` | `crdop` | `[networks] crdop=yes` — merge `share/hybbx/crdop-host.ini.example` | + +Frozen: `docs/BAYCOM.md`, `docs/CRDOP.md` · MAX25 contract: `docs/HYBBX.md`. Public mark for SER12 class: **BayCom/based** (never Konverter/converter). + +--- + +## 6. Clients + +| Client | Transport | Default port | +|--------|-----------|--------------| +| `hybbx-telnet` | TCP | 2323 | +| `hybbx-ssh` | SSH (libssh) | 3232 | +| `hybbx-terminal` | HBX circuit | 7323 | +| Web browser | WebSocket via reverse proxy | per site | + +Frozen: `docs/CLIENTS.md`, `docs/WEBSOCKET.md`. + +--- + +## 7. Commands and access levels + +Five levels: Sysop → Admin → Mod → User → Guest. `/help`, `/menu` filtered by level; `/index` lists all commands for every account. + +| Sysop RF | `/broadcast`, `/broadcast ax25`, `/shutdown` | +|----------|-----------------------------------------------| +| Admin | `/usercreate`, `/activate`, `/promote`, `/demote` | + +Frozen: `docs/COMMANDS.md`, `share/commands.yaml`, `share/areas.yaml`. + +--- + +## 8. Operator start order (with MAX25) + +Linear sequence for standalone Main + local TNC: + +1. **MAX25 prep** — boot-wait or `max25d` per TNC port (or verify `ss -ltn | grep 7325`) +2. **HyBBX** — `hybbxd -c hybbx.ini` with `kiss_entry=none`, `[max25] check=yes` +3. **Verify** — log: `max25d reachable` → `KISS attach (MAX25 prep assumed)` → `RF TX` on `/broadcast ax25` +4. **RF check** — PTT and on-air decode at remote station + +Integration detail: [DUAL-TNC-OPERATOR-GUIDE.md](DUAL-TNC-OPERATOR-GUIDE.md). + +--- + +## 9. Future roadmap (deferred) + +Aligned with MAX25 [V2.0.0-SCOPE](../../10-PROJECTS/docs/V2.0.0-SCOPE.md) **Later** row — **no product commits** until operator orders a separate track. + +| Later | Status | +|-------|--------| +| **`/aichat`** — AI / assistant session command | **deferred** | +| AI / assistant integration (backends, APIs) | **deferred** | +| like-features | **deferred** | + +Full map: [2026-07-14-hybbx-future-roadmap.md](2026-07-14-hybbx-future-roadmap.md). + +**v2.8.0:** `/who` lists **interactive users only** (telnet/SSH/WebSocket) — plugin/RF connection sessions are not users. + +--- + +## Related + +| Topic | Path | +|-------|------| +| RF investigation SSoT | [2026-07-12-site-rf-broadcast-investigation.md](2026-07-12-site-rf-broadcast-investigation.md) | +| tnc.c recovery integration | [2026-07-13-tnc-recovery-integration.md](2026-07-13-tnc-recovery-integration.md) | +| INI operator notes | [2026-07-12-ini-operator-notes.md](2026-07-12-ini-operator-notes.md) | +| MAX25 boundary | [../integration/2026-07-12-max25-hybbx-boundary-final.md](../integration/2026-07-12-max25-hybbx-boundary-final.md) | +| Dual-TNC operator flow | [DUAL-TNC-OPERATOR-GUIDE.md](DUAL-TNC-OPERATOR-GUIDE.md) | +| Frozen product docs | `docs/` (read-only) | +| Production INI (vault) | `share/` INI examples | +| TNC hardware SSoT | [hardware/tnc2c/CONFIRMED.md](../../hardware/tnc2c/CONFIRMED.md) | diff --git a/docs/PLATFORMS.md b/docs/PLATFORMS.md new file mode 100644 index 0000000..f74ff86 --- /dev/null +++ b/docs/PLATFORMS.md @@ -0,0 +1,28 @@ +# Platforms · HyBBX 2.8.0 + +POSIX+ first — Linux, *BSD, AmigaOS 3.9+, macOS, Windows. + +## Platform matrix + +| Platform | Daemon | Telnet client | packet_radio | SSH | +|----------|--------|---------------|--------------|-----| +| Linux | yes | yes | yes | yes (libssh) | +| FreeBSD / NetBSD / OpenBSD | yes | yes | yes | yes | +| AmigaOS 3.9+ | partial | yes | yes | no | +| macOS | yes | yes | yes | yes | +| Windows | yes | yes | limited | yes | + +## AmigaOS matrix + +| Item | Value | +|------|-------| +| Cross-build | `./scripts/build-amiga-telnet.sh` | +| Daemon plugins | telnet + packet_radio only | +| Full feature set | Linux / *BSD recommended | + +## Related + +| Goal | Doc | +|------|-----| +| Build | [BUILD.md](BUILD.md) | +| Manual | [MANUAL.md](MANUAL.md) | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..bff6b9c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,32 @@ +# Documentation index · HyBBX 2.8.0 + +One-line role: compact doc map — start at MASTER-GUIDE for linear operator flow. + +## Doc index matrix + +| Role | Document | +|------|----------| +| Master operator guide | [MASTER-GUIDE.md](MASTER-GUIDE.md) | +| Dual-TNC operator flow | [DUAL-TNC-OPERATOR-GUIDE.md](DUAL-TNC-OPERATOR-GUIDE.md) | +| Full manual | [MANUAL.md](MANUAL.md) | +| Topology | [TOPOLOGY.md](TOPOLOGY.md) | +| TNC profiles | [TNCS.md](TNCS.md) | +| Clients | [CLIENTS.md](CLIENTS.md) | +| Commands | [COMMANDS.md](COMMANDS.md) | +| BayCom transport plugin | [BAYCOM.md](BAYCOM.md) | +| CRDOP | [CRDOP.md](CRDOP.md) | +| ARDOP | [ARDOP.md](ARDOP.md) | +| Build | [BUILD.md](BUILD.md) | +| Platforms | [PLATFORMS.md](PLATFORMS.md) | +| Security | [SECURITY.md](SECURITY.md) | +| WebSocket | [WEBSOCKET.md](WEBSOCKET.md) | +| Developer | [DEVELOPMENT.md](DEVELOPMENT.md) | + +## Related + +| Goal | Doc | +|------|-----| +| Build | [BUILD.md](BUILD.md) | +| RF attach | [TNCS.md](TNCS.md) | + +- [TMODEM.md](TMODEM.md) — T-Modem-c1224 attach via MAX25 diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..ed0b3f0 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,38 @@ +# Security · HyBBX 2.8.0 + +Built-in `[security]` — network protection and abuse in one subsystem. + +## Layer matrix + +| Layer | What | Ban? | +|-------|------|------| +| **Soft limits** | RF pacing, message size, traffic shaping | **No** | +| **Abuse** | Brute-force, connection flood, excessive spam | **Yes** — short IP/CALLID cool-down | + +## Soft limit matrix + +| Area | Keys | Effect | +|------|------|--------| +| `[traffic]` | `baud`, `pace_output`, `line_width` | Output pacing | +| `[chat]` | `message_max` | Truncate oversized lines | +| `[mail]` | `max_messages`, `body_max` | Mailbox caps | +| AX.25 broadcast | fixed | Min 900 s auto cycle; 60 s between links | + +## Ban trigger matrix + +| Target | Event | Default threshold | +|--------|-------|-------------------| +| **IP** | `login_fail` | 5 / 10 min | +| **IP** | `link_auth_fail` | 5 / 10 min | +| **IP** | `rate_limit` | 30 / 60 s | +| **CALLID** | `link_auth_fail` | same as login | +| **CALLID** | `ban_callid=` config | immediate, permanent | + +CALLID = AX.25 callsign or HBX `link_id`. Optional `iptables`/`nftables` via `ban_backend`. + +## Related + +| Goal | Doc | +|------|-----| +| Manual `[security]` | [MANUAL.md](MANUAL.md) | +| Topology link auth | [TOPOLOGY.md](TOPOLOGY.md) | diff --git a/docs/TMODEM.md b/docs/TMODEM.md new file mode 100644 index 0000000..71aa0ed --- /dev/null +++ b/docs/TMODEM.md @@ -0,0 +1,32 @@ +# T-Modem-c1224 — HyBBX attach + +**AS-IS** · prototype. Public term: **BBX** (not BBS). Never **AX25SRV** in public copy. + +## Role + +HyBBX does **not** run T-Modem L1. Attach to a running **max25d** that has device id **`tmodem`** enabled (USB KISS half-TNC from T-Modem-c1224 Pico firmware). + +| Layer | Owns | +|-------|------| +| T-Modem-c1224 FW | HDLC + bitbang + chip AFSK + PTT | +| MAX25-Stack max25d | Device `tmodem` · `kiss-raw-serial` on `/dev/ttyACM*` | +| HyBBX | Network/BBX attach to max25d only | + +Not BayCom/based. Not SER12. + +## Example (default off) + +```ini +[networks] +tmodem = no +``` + +When max25d already serves `tmodem`, prefer the existing **max25** / host attach path used for other MAX25 devices — enable only after max25d RX proof (RX before TX). + +## Related + +| Doc | Path | +|-----|------| +| MAX25 DEV | MAX25-Stack `docs/DEV/TMODEM-C1224.md` | +| BayCom/based (different) | [BAYCOM.md](BAYCOM.md) | +| TNCs | [TNCS.md](TNCS.md) | diff --git a/docs/TNCS.md b/docs/TNCS.md new file mode 100644 index 0000000..c2b156b --- /dev/null +++ b/docs/TNCS.md @@ -0,0 +1,56 @@ +# TNC profiles · HyBBX 2.8.0 + +Single `packet_radio` transport — up to 8 instances per process. MAX25 owns RF prep; HyBBX attach-only in v2.8.0. + +## packet_radio / MAX25 matrix + +| Rule | Value | +|------|-------| +| `kiss_entry` | `none` (default) — MAX25 sends `kiss on` | +| `[max25] check` | `yes` — probe max25d `:7325` before serial open | +| Serial ownership | One process per `/dev/tty*` | + +## Host protocol matrix + +| Protocol | INI `protocol=` | Use | +|----------|-----------------|-----| +| KISS | `kiss` (default) | Secondary RF — recommended after MAX25 prep | +| TNC2 host converse | `hostmode`, `host`, `tnc2` | Interactive host session | +| 6PACK | `sixpack` | DF6BU 6PACK over serial | + +## KISS entry/exit matrix + +| INI key | Values | v2.8.0 default | +|---------|--------|----------------| +| `kiss_entry` | `none`, `kiss_on`, `esc_at_k`, `auto` | `none` | +| `kiss_exit` | `none`, `kiss_off`, `kiss_frame`, `auto` | `none` | + +## TNC profile matrix (packet_radio) + +| Profile | Aliases | Serial | KISS attach | RTS/DTR | +|---------|---------|--------|-------------|---------| +| `tnc2c` | `tnc2-c` | 7E1 | `none` | boot-wait | +| `tnc2` | `pktnc2`, `pk-tnc2`, `tapr`, `thefirmware` | 8N1 | `none` | boot-wait | +| `pk232` | `pk-232`, `aea` | 8N1 | `none` | off | +| `mfj1278` | `mfj-1278` | 7E1 | `none` | off | +| `kantronics` | `kpc`, `kpc3` | 8N1 | `none` | off | +| `generic` | `tnc` | 8N1 | `none` | off | + +BayCom hardware: optional [`baycom`](BAYCOM.md) transport plugin — not a `packet_radio` TNC profile. + +## AX.25 source matrix + +| Item | Owner | +|------|-------| +| Host `MYCALL` on TNC | MAX25 (prep) | +| Outbound UI source | HyBBX `[broadcast] ax25_mycall` | +| KISS DATA FCS | Stripped by HyBBX — TNC adds CRC on air | +| `persist` on CB | `255` recommended — CSMA defer avoidance | + +## Related + +| Goal | Doc | +|------|-----| +| Manual | [MANUAL.md](MANUAL.md) | +| BayCom plugin | [BAYCOM.md](BAYCOM.md) | +| Dual-TNC | [DUAL-TNC-OPERATOR-GUIDE.md](DUAL-TNC-OPERATOR-GUIDE.md) | diff --git a/docs/TOPOLOGY.md b/docs/TOPOLOGY.md new file mode 100644 index 0000000..0336c9d --- /dev/null +++ b/docs/TOPOLOGY.md @@ -0,0 +1,43 @@ +# Topology · HyBBX 2.8.0 + +Main, Secondary, and mains-proxy mesh layout. + +## Role matrix + +| Role | Process | Typical `[networks]` | Hosts | +|------|---------|----------------------|-------| +| **Main** | `hybbxd` | `circuit=yes` | Users, storage, HBX hub `:7323` | +| **Secondary** | `hybbxd` (remote) | `circuit=no`, `ax25=yes` | RF; HBX client to Main | +| **Standalone Main** | `hybbxd` | `ax25=yes` on same box | Lab / dual local TNC | + +## RF attachment matrix + +| Layout | TNC host | Main `[transport.packet_radioN]` | +|--------|----------|----------------------------------| +| Remote Secondary | Secondary (`device`, `tnc`, `circuit_host`) | Bridge registry only (`link_id`, password) | +| Local TNC on Main | Same box as users | Full TNC keys (`device`, `tnc`, …) | + +## HBX/Circuit matrix + +| Item | Value | +|------|-------| +| Protocol | HBX v1 — magic `HBX` | +| Default port | `7323` | +| Max links | 16 (default `max_links=8`) | +| Auth | Per-link `link_password` | +| Scope | Sole inter-node transport — core never sees raw KISS | + +## Broadcast matrix + +| Command | Scope | Level | +|---------|-------|-------| +| `/broadcast ` | Local logged-in users | Sysop | +| `/broadcast ax25` | Sequential RF beacon (60 s link gap) | Sysop | + +## Related + +| Goal | Doc | +|------|-----| +| Manual | [MANUAL.md](MANUAL.md) | +| Mesh proxy | [MAINS_PROXY.md](MAINS_PROXY.md) | +| Dual-TNC | [DUAL-TNC-OPERATOR-GUIDE.md](DUAL-TNC-OPERATOR-GUIDE.md) | diff --git a/docs/WEBSOCKET.md b/docs/WEBSOCKET.md new file mode 100644 index 0000000..7a19b03 --- /dev/null +++ b/docs/WEBSOCKET.md @@ -0,0 +1,27 @@ +# WebSocket · HyBBX 2.8.0 + +WebSocket forward-proxy for browser and rich clients — core stays text-first. + +## Transport matrix + +| Item | Value | +|------|-------| +| Plugin | `HYBBX_PLUGIN_WEBSOCKET=ON` (default) | +| Default port | `4591` (plain); wss with OpenSSL | +| INI | `[networks] websocket=yes`, `[transport.websocket]` | +| Reverse proxy | nginx/caddy TLS termination typical | + +## Security matrix + +| Item | Value | +|------|-------| +| User auth | Same as telnet — session login | +| TLS | Optional `-DHYBBX_CRYPTO_OPENSSL=ON` for wss | +| Link auth | Separate from HBX circuit auth | + +## Related + +| Goal | Doc | +|------|-----| +| Clients | [CLIENTS.md](CLIENTS.md) | +| Security | [SECURITY.md](SECURITY.md) | diff --git a/include/hybbx/ardop.h b/include/hybbx/ardop.h new file mode 100644 index 0000000..b0869b0 --- /dev/null +++ b/include/hybbx/ardop.h @@ -0,0 +1,50 @@ +#ifndef HYBBX_ARDOP_H +#define HYBBX_ARDOP_H + +/** + * ARDOP link adapter — HyBBX Host-Client subset (external ARDOP modem). + * + * HyBBX is never a sound-modem service. Operator runs ARDOPC or ardopcf as a + * separate process; this plugin speaks the TNC Host Interface over TCP. + * For CRDOP, use the `crdop` plugin and external CRDOPC instead. + */ + +#include "hybbx/types.h" +#include "hybbx/crdop.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default ARDOPC TCP control port (data = port + 1). */ +#define HYBBX_ARDOP_DEFAULT_PORT 8515u + +/** Max payload per ARDOP host data frame (spec timing guidance). */ +#define HYBBX_ARDOP_DATA_MAX 2000u + +typedef struct hybbx_ardop_config { + char *ardop_host; + unsigned ardop_port; + char *mycall; + char *arq_bandwidth; + char *peer_call; + int listen; + char *circuit_host; + unsigned circuit_port; + char *link_id; + char *link_password; + char *link_role; + double frequency_mhz; + /** Experimental CRDOP preview: cb | amateur (see docs/CRDOP.md). */ + hybbx_crdop_radio_profile_t radio_profile; +} hybbx_ardop_config_t; + +hybbx_result_t hybbx_ardop_config_parse(const char *config, + hybbx_ardop_config_t *out); +void hybbx_ardop_config_free(hybbx_ardop_config_t *config); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_ARDOP_H */ diff --git a/include/hybbx/auth.h b/include/hybbx/auth.h new file mode 100644 index 0000000..10c27ce --- /dev/null +++ b/include/hybbx/auth.h @@ -0,0 +1,117 @@ +#ifndef HYBBX_AUTH_H +#define HYBBX_AUTH_H + +#include "hybbx/password.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_AUTH_GUEST_PREFIX_MAX 32 + +/** Default guest name prefix: Guest1, Guest2, … */ +#define HYBBX_AUTH_DEFAULT_GUEST_PREFIX "Guest" + +/** Highest guest number issued (Guest1 … Guest25); max simultaneous guests. */ +#define HYBBX_GUEST_NUMBER_MAX 25u + +/** + * Synthetic user IDs for ephemeral guest sessions (not stored in user files). + * Slot @p n is 1 … @ref HYBBX_GUEST_NUMBER_MAX. + */ +#define HYBBX_GUEST_USER_ID(n) (0xffffffffffffff00ULL + (uint64_t)(n)) + +#define HYBBX_USERNAME_MIN_LEN 4u +#define HYBBX_USERNAME_MAX_LEN 12u +#define HYBBX_USERNAME_MAX_DIGITS 4u + +/** Default Sysop account (auto-created when no Sysop exists in the user database). */ +#define HYBBX_DEFAULT_SYSOP_USERNAME "Sysop" +#define HYBBX_SYSOP_INIT_PASSWORD_MIN 10u +#define HYBBX_SYSOP_INIT_PASSWORD_MAX 14u + +/** Plain-text password length policy for new passwords (/changeme, future set-password). */ +#define HYBBX_PASSWORD_MIN_LEN 8u +#define HYBBX_PASSWORD_MAX_LEN 24u + +/** + * User privilege levels (highest to lowest). + * Sysop: exactly one account on the system. + */ +typedef enum hybbx_user_level { + HYBBX_LEVEL_SYSOP = 1, + HYBBX_LEVEL_ADMIN = 2, + HYBBX_LEVEL_MOD = 3, + HYBBX_LEVEL_USER = 4, + HYBBX_LEVEL_GUEST = 5 +} hybbx_user_level_t; + +typedef struct hybbx_auth_config { + int auto_login; + char guest_prefix[HYBBX_AUTH_GUEST_PREFIX_MAX]; +} hybbx_auth_config_t; + +void hybbx_auth_config_defaults(hybbx_auth_config_t *auth); + +const char *hybbx_user_level_name(hybbx_user_level_t level); +hybbx_user_level_t hybbx_user_level_parse(const char *name); +int hybbx_user_level_is_guest(hybbx_user_level_t level); + +/** Non-zero when @p level is Sysop or Admin. */ +int hybbx_user_level_is_sysop_or_admin(hybbx_user_level_t level); + +/** Non-zero when @p level is the protected Sysop account tier. */ +int hybbx_user_level_is_sysop(hybbx_user_level_t level); + +/** Non-zero when @p password meets the plain-text password policy (8–24 characters). */ +int hybbx_password_plain_valid(const char *password); + +/** + * Return non-zero when @p username is valid for registration: + * 4–12 characters, letters (a–z), digits (max 4), and at most one `_` or one `-` + * (not both); case-insensitive. + */ +int hybbx_username_valid(const char *username, const char *guest_prefix); + +/** Fold @p username to lowercase in place (for case-insensitive lookup). */ +void hybbx_username_normalize(char *username); + +/** Presentation form for output (e.g. Sysop instead of stored sysop). */ +const char *hybbx_username_display(const char *username, + hybbx_user_level_t level); + +struct hybbx_user_record; + +/** User-facing display name (nickname when set, else username). */ +const char *hybbx_user_display_name(const struct hybbx_user_record *user); + +/** + * Derive a default nickname from a legacy stored username when none is set. + */ +void hybbx_nickname_infer(const char *stored_username, + char *nickname, + size_t nickname_len); + +/** + * Parse @p username as @c <1-25> (case-insensitive prefix). + * Writes slot to @p slot_out. Returns 1 on match, else 0. + */ +int hybbx_guest_slot_from_username(const char *guest_prefix, + const char *username, + unsigned *slot_out); + +struct hybbx_user_record; + +/** Fill @p out with an ephemeral guest record for @p slot (1 … 25). */ +void hybbx_guest_fill_record(const char *guest_prefix, + unsigned slot, + struct hybbx_user_record *out); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_AUTH_H */ diff --git a/include/hybbx/ax25.h b/include/hybbx/ax25.h new file mode 100644 index 0000000..753de15 --- /dev/null +++ b/include/hybbx/ax25.h @@ -0,0 +1,68 @@ +#ifndef HYBBX_AX25_H +#define HYBBX_AX25_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_AX25_CALL_MAX 6 +#define HYBBX_AX25_SSID_MAX 15 +#define HYBBX_AX25_ADDR_ENCODED 7 +#define HYBBX_AX25_MAX_DIGI 8 +#define HYBBX_AX25_FRAME_MAX 330 +#define HYBBX_AX25_PID_NOL3 0xF0u +#define HYBBX_AX25_CTRL_UI 0x03u + +typedef struct hybbx_ax25_address { + char call[HYBBX_AX25_CALL_MAX + 1]; + unsigned ssid; +} hybbx_ax25_address_t; + +typedef struct hybbx_ax25_path { + hybbx_ax25_address_t dest; + hybbx_ax25_address_t source; + hybbx_ax25_address_t digi[HYBBX_AX25_MAX_DIGI]; + unsigned digi_count; +} hybbx_ax25_path_t; + +/** + * Parse "CALL" or "CALL-SSID" into @p out. + */ +hybbx_result_t hybbx_ax25_address_parse(const char *text, + hybbx_ax25_address_t *out); + +/** + * Encode one AX.25 address field (7 bytes). + * @p last non-zero on the final address field before control byte. + */ +void hybbx_ax25_encode_address(uint8_t *out, const hybbx_ax25_address_t *addr, + int last); + +/** CRC-CCITT (0x1021), init 0xFFFF — AX.25 FCS. */ +uint16_t hybbx_ax25_crc(const uint8_t *data, size_t len); + +/** + * Build an AX.25 UI frame (PID 0xF0) into @p out. + * @return frame length or 0 on error. + */ +size_t hybbx_ax25_build_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap); + +/** + * Parse a UI frame. Returns payload length or 0 if not a UI frame / invalid FCS. + */ +size_t hybbx_ax25_parse_ui(const uint8_t *frame, size_t frame_len, + hybbx_ax25_path_t *path, + uint8_t *payload, size_t payload_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_AX25_H */ diff --git a/include/hybbx/bandwidth_policy.h b/include/hybbx/bandwidth_policy.h new file mode 100644 index 0000000..6c12296 --- /dev/null +++ b/include/hybbx/bandwidth_policy.h @@ -0,0 +1,35 @@ +#ifndef HYBBX_BANDWIDTH_POLICY_H +#define HYBBX_BANDWIDTH_POLICY_H + +/** + * Per-user bandwidth limits when a low-bandwidth circuit link is under pressure. + * Circuit (secondary) sessions are never targeted. Among users, AX.25 sessions + * are sacrificed before full-duplex TCP/telnet; within each class, newest first. + */ + +#include "hybbx/circuit_balance.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_BANDWIDTH_DISCONNECT_MSG "You are disconnected by bandwidth limits." + +struct hybbx_service; + +/** Logged-in telnet and packet_radio users subject to limits (not circuit links). */ +unsigned hybbx_bandwidth_policy_user_count(struct hybbx_service *service); + +/** + * Apply pause / break / cancel / resume to online users (AX.25 before TCP, then LIFO). + * Circuit (secondary) sessions are never targeted. Returns users affected. + */ +unsigned hybbx_bandwidth_policy_apply(struct hybbx_service *service, + hybbx_circuit_balance_action_t action); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_BANDWIDTH_POLICY_H */ diff --git a/include/hybbx/baycom.h b/include/hybbx/baycom.h new file mode 100644 index 0000000..a351e55 --- /dev/null +++ b/include/hybbx/baycom.h @@ -0,0 +1,82 @@ +#ifndef HYBBX_BAYCOM_H +#define HYBBX_BAYCOM_H + +/** + * BayCom PR-Stack link adapter — Linux kernel HDLC modem (SER12 / PAR96 / EPP) + * or serial KISS firmware path. + * + * Native PR-Stack L2 uses Thomas Sailer kernel drivers (baycom_ser_fdx, + * baycom_ser_hdx, baycom_par, baycom_epp) and hdlcdrv channel access. + * See docs/BAYCOM.md. + */ + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default Linux baycom_ser_fdx netdev (SER12 fullduplex). */ +#define HYBBX_BAYCOM_DEFAULT_INTERFACE "bcsf0" + +/** Default SER12 UART I/O base (COM1). */ +#define HYBBX_BAYCOM_DEFAULT_IOBASE 0x3f8u + +/** Default SER12 IRQ (COM1). */ +#define HYBBX_BAYCOM_DEFAULT_IRQ 4u + +/** Default SER12 on-air baud (1200 AFSK). */ +#define HYBBX_BAYCOM_DEFAULT_RADIO_BAUD 1200u + +typedef enum hybbx_baycom_backend { + HYBBX_BAYCOM_BACKEND_KERNEL = 1, + HYBBX_BAYCOM_BACKEND_KISS = 2, +} hybbx_baycom_backend_t; + +typedef enum hybbx_baycom_modem_mode { + HYBBX_BAYCOM_MODE_SER12 = 1, + HYBBX_BAYCOM_MODE_SER12_SOFTDCD, + HYBBX_BAYCOM_MODE_SER12_INV, + HYBBX_BAYCOM_MODE_SER12HDX, + HYBBX_BAYCOM_MODE_PAR96, + HYBBX_BAYCOM_MODE_PAR96_SOFTDCD, + HYBBX_BAYCOM_MODE_EPP, +} hybbx_baycom_modem_mode_t; + +typedef struct hybbx_baycom_config { + hybbx_baycom_backend_t backend; + hybbx_baycom_modem_mode_t mode; + char *interface; + char *kernel_module; + unsigned iobase; + unsigned irq; + unsigned radio_baud; + int kernel_autoload; + unsigned txdelay; + unsigned txtail; + unsigned slot; + unsigned persist; + int full_duplex; + char *device; + unsigned serial_baud; + char *mycall; + char *circuit_host; + unsigned circuit_port; + char *link_id; + char *link_password; + char *link_role; + char *frequency_mhz; +} hybbx_baycom_config_t; + +hybbx_result_t hybbx_baycom_config_parse(const char *config, + hybbx_baycom_config_t *out); +void hybbx_baycom_config_free(hybbx_baycom_config_t *config); + +const char *hybbx_baycom_modem_mode_name(hybbx_baycom_modem_mode_t mode); +const char *hybbx_baycom_backend_name(hybbx_baycom_backend_t backend); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_BAYCOM_H */ diff --git a/include/hybbx/broadcast.h b/include/hybbx/broadcast.h new file mode 100644 index 0000000..a4002b7 --- /dev/null +++ b/include/hybbx/broadcast.h @@ -0,0 +1,110 @@ +#ifndef HYBBX_BROADCAST_H +#define HYBBX_BROADCAST_H + +/** + * HyBBX broadcast — Main instance only. + * + * User command `/broadcast` (alias `/announce`): instant `+++` message to every + * online local user session on this Main (telnet/SSH/WebSocket). + * Format: `+++ : ` (manual Sysop/grant announce). + * Automatic system notices use `***` instead. + * + * `/broadcast ax25`: manual instant RF beacon — ax25_auto_message to each + * qualifying packet-radio link sequentially (no custom RF text). + * + * INI `[broadcast]` ax25_auto: periodic AX.25 QST UI beacon over HBX to + * Secondary extenders (low-bandwidth + half-duplex links). Separate from + * `/broadcast`; not a user command. One sequential cycle per interval (min + * 900 s); 180 s own-channel idle before each link TX; 180 s between links; + * per-link min 900 s. Busy channel defers until idle. + */ + +#include "hybbx/ax25.h" +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_AX25_FREQUENCY_MAX 40u +/** Local Main announce message cap. */ +#define HYBBX_BROADCAST_MESSAGE_MAX 240u + +#define HYBBX_BROADCAST_AUTO_MESSAGE_DEFAULT "Broadcast: @service@ online" + +typedef struct hybbx_ax25_frequency_table { + unsigned count; + double mhz[HYBBX_AX25_FREQUENCY_MAX]; + char labels[HYBBX_AX25_FREQUENCY_MAX][32]; +} hybbx_ax25_frequency_table_t; + +typedef struct hybbx_broadcast_config { + int enabled; + int ax25_enabled; + int ax25_auto; + unsigned ax25_auto_interval_sec; + /** Ignored (legacy INI); links always send sequentially with link gap. */ + unsigned ax25_auto_stagger_sec; + char ax25_mycall[HYBBX_AX25_CALL_MAX + 1]; + char ax25_dest[HYBBX_AX25_CALL_MAX + 1]; + char ax25_auto_message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + hybbx_ax25_frequency_table_t frequencies; +} hybbx_broadcast_config_t; + +struct hybbx_service; +struct hybbx_session; + +void hybbx_ax25_frequency_table_clear(hybbx_ax25_frequency_table_t *table); + +/** Load `[ax25]` frequency1 … frequencyN (MHz) and optional labels. */ +void hybbx_ax25_frequency_apply(hybbx_ax25_frequency_table_t *table, + const hybbx_config_t *config); + +/** Compare two MHz values (tolerance for INI rounding). */ +int hybbx_ax25_frequency_match(double a_mhz, double b_mhz); + +void hybbx_broadcast_config_defaults(hybbx_broadcast_config_t *cfg); + +/** Load `[broadcast]` + `[ax25]` from INI. */ +void hybbx_broadcast_config_apply(hybbx_broadcast_config_t *cfg, + const hybbx_config_t *config); + +const hybbx_broadcast_config_t *hybbx_service_get_broadcast( + const struct hybbx_service *service); + +/** + * Send @p message to every online local user on this Main (Sysop `/broadcast`). + */ +hybbx_result_t hybbx_broadcast_announce(struct hybbx_service *service, + struct hybbx_session *from, + const char *message); + +/** + * Manual AX.25 beacon (Sysop `/broadcast ax25`): INI ax25_auto_message to each + * qualifying link immediately, one after another. No custom RF text. + */ +hybbx_result_t hybbx_broadcast_ax25_manual(struct hybbx_service *service); + +/** + * AX.25 auto-beacon over HBX (INI ax25_auto only). + * Link must be low-bandwidth and half-duplex (QoS). + */ +hybbx_result_t hybbx_broadcast_ax25(struct hybbx_service *service, + double frequency_mhz, + const char *message); + +/** + * Periodic AX.25 auto-beacon (call once per second from Main service loop). + */ +void hybbx_broadcast_ax25_tick(struct hybbx_service *service); + +/** Abort an in-flight sequential AX.25 cycle (hub stop / shutdown). */ +void hybbx_broadcast_ax25_seq_cancel(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_BROADCAST_H */ diff --git a/include/hybbx/chat.h b/include/hybbx/chat.h new file mode 100644 index 0000000..4f03cb4 --- /dev/null +++ b/include/hybbx/chat.h @@ -0,0 +1,82 @@ +#ifndef HYBBX_CHAT_H +#define HYBBX_CHAT_H + +#include "hybbx/config.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; +struct hybbx_session; + +/** Hard limit on chat channels (also the default configured count). */ +#define HYBBX_CHAT_CHANNEL_MAX 10u + +#define HYBBX_CHAT_CHANNEL_NAME_MAX 32 + +#define HYBBX_DEFAULT_CHAT_CHANNELS HYBBX_CHAT_CHANNEL_MAX + +/** Maximum characters per chat message line (fits 80-column wrap). */ +#define HYBBX_CHAT_MESSAGE_MAX 72u + +typedef struct hybbx_chat_config { + /** Active channels (1 … @ref HYBBX_CHAT_CHANNEL_MAX). */ + unsigned channel_count; + /** Maximum characters per chat message line. */ + unsigned message_max; + /** + * Channel names (index 0 = channel 1). Default: Channel1 … Channel10. + * Override with [chat] channel1 … channel10 in INI. + */ + char names[HYBBX_CHAT_CHANNEL_MAX][HYBBX_CHAT_CHANNEL_NAME_MAX]; +} hybbx_chat_config_t; + +void hybbx_chat_config_defaults(hybbx_chat_config_t *chat); + +/** Load `[chat]` settings from an INI file. */ +void hybbx_chat_config_apply(hybbx_chat_config_t *chat, + const hybbx_config_t *config); + +const hybbx_chat_config_t *hybbx_service_get_chat(const struct hybbx_service *service); + +/** Return channel name for @p channel_index (1-based), or NULL when invalid. */ +const char *hybbx_chat_channel_name(const hybbx_chat_config_t *chat, + unsigned channel_index); + +/** + * Resolve @p spec as channel number (1-based) or name (case-insensitive). + * @p out_index receives 1-based channel index on success. + */ +hybbx_result_t hybbx_chat_resolve_channel(const hybbx_chat_config_t *chat, + const char *spec, + unsigned *out_index); + +/** List configured channels on @p session. */ +void hybbx_chat_list_channels(struct hybbx_session *session, + const hybbx_chat_config_t *chat); + +/** Broadcast a chat line within the sender's channel. + * Sender sees "ME: …"; others see ": …". + */ +hybbx_result_t hybbx_chat_post(struct hybbx_service *service, + struct hybbx_session *from, + const char *message); + +/** List display names in the requester's current chat channel only. */ +void hybbx_chat_show_channel(struct hybbx_service *service, + struct hybbx_session *session); + +/** + * List all users in public chat channels as nick@ChannelN, sorted by channel, + * wrapped at 80 columns (conference sessions excluded). + */ +void hybbx_chat_show_all(struct hybbx_service *service, + struct hybbx_session *session); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CHAT_H */ diff --git a/include/hybbx/circuit.h b/include/hybbx/circuit.h new file mode 100644 index 0000000..c732602 --- /dev/null +++ b/include/hybbx/circuit.h @@ -0,0 +1,119 @@ +#ifndef HYBBX_CIRCUIT_H +#define HYBBX_CIRCUIT_H + +/** + * HBX — Hybrid Bridge eXchange (v1). + * + * HyBBX internal framed protocol on the circuit TCP hub. Multiplexes + * link-layer payloads (AX.25 and future stacks) and application streams + * (terminal, proxymail, proxychat) between HyBBX processes. Header magic: + * `H` `B` `X`. Transport is internal TCP/IPv4+IPv6 only — the application + * core never sees KISS, AX.25 on-air framing, or serial. + * + * Short form in docs: **HBX** or **HBX/Circuit** — Circuit = TCP hub + * (:7323), HBX = framing on top. + */ + +#include "hybbx/ax25.h" +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_CIRCUIT_MAGIC_0 'H' +#define HYBBX_CIRCUIT_MAGIC_1 'B' +#define HYBBX_CIRCUIT_MAGIC_2 'X' +#define HYBBX_CIRCUIT_VERSION 0x01u + +#define HYBBX_CIRCUIT_HEADER_SIZE 12u +#define HYBBX_CIRCUIT_MAX_PAYLOAD 4096u +#define HYBBX_CIRCUIT_MAX_FRAME (HYBBX_CIRCUIT_HEADER_SIZE + HYBBX_CIRCUIT_MAX_PAYLOAD) + +/** Default internal circuit TCP port (loopback). */ +#define HYBBX_CIRCUIT_DEFAULT_PORT 7323u + +/** + * Protocol identifiers — extensible for future link stacks. + * Values 0x01..0x7F: link-layer payloads; 0x80..0xFF: application streams. + */ +typedef enum hybbx_circuit_proto { + HYBBX_CIRCUIT_PROTO_AX25 = 0x01, /**< Raw AX.25 frame incl. FCS */ + HYBBX_CIRCUIT_PROTO_AX25_UI = 0x02, /**< Masked UI: path + payload */ + HYBBX_CIRCUIT_PROTO_LINK_AUTH = 0x03, /**< Edge link password auth (no ping) */ + HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK = 0x04,/**< Central auth response */ + HYBBX_CIRCUIT_PROTO_FLOW_CTRL = 0x05, /**< Load-balance pause/break/cancel */ + HYBBX_CIRCUIT_PROTO_TERMINAL = 0x10, /**< BBX / terminal byte stream */ + HYBBX_CIRCUIT_PROTO_PROXY_MAIL = 0x80, /**< Inter-Main proxymail (service only) */ + HYBBX_CIRCUIT_PROTO_PROXY_CHAT = 0x81, /**< Inter-Main proxychat (service only) */ + HYBBX_CIRCUIT_PROTO_RESERVED_APRS = 0x20, + HYBBX_CIRCUIT_PROTO_RESERVED_NETROM = 0x21 +} hybbx_circuit_proto_t; + +typedef enum hybbx_circuit_flags { + HYBBX_CIRCUIT_FLAG_NONE = 0x0000, + HYBBX_CIRCUIT_FLAG_RX = 0x0001, /**< Frame originated on RF RX */ + HYBBX_CIRCUIT_FLAG_TX = 0x0002, /**< Frame destined for RF TX */ + HYBBX_CIRCUIT_FLAG_PATH = 0x0004, /**< AX.25_UI payload carries path */ + HYBBX_CIRCUIT_FLAG_G3RUH_FSK = 0x0008 /**< G3RUH 9600 FSK on-air (vs AFSK) */ +} hybbx_circuit_flags_t; + +typedef void (*hybbx_circuit_frame_cb)(hybbx_circuit_proto_t proto, + uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata); + +typedef struct hybbx_circuit_decoder { + uint8_t buf[HYBBX_CIRCUIT_MAX_FRAME]; + size_t len; + size_t need; + int have_header; + hybbx_circuit_proto_t proto; + uint16_t flags; +} hybbx_circuit_decoder_t; + +void hybbx_circuit_decoder_init(hybbx_circuit_decoder_t *dec); + +void hybbx_circuit_decoder_feed(hybbx_circuit_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_circuit_frame_cb cb, void *userdata); + +size_t hybbx_circuit_encode(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap); + +size_t hybbx_circuit_encode_ax25(const uint8_t *frame, size_t frame_len, + uint16_t flags, + uint8_t *out, size_t out_cap); + +size_t hybbx_circuit_encode_ax25_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint16_t flags, + uint8_t *out, size_t out_cap); + +size_t hybbx_circuit_encode_terminal(const char *data, size_t len, + uint8_t *out, size_t out_cap); + +/** Encode LINK_AUTH or LINK_AUTH_ACK line-oriented payload. */ +size_t hybbx_circuit_encode_link_msg(hybbx_circuit_proto_t proto, + const char *payload, size_t payload_len, + uint8_t *out, size_t out_cap); + +/** + * Unpack AX.25_UI masked payload into path + UI bytes. + * @return UI payload length or 0 on error. + */ +size_t hybbx_circuit_unpack_ax25_ui(const uint8_t *payload, size_t payload_len, + hybbx_ax25_path_t *path, + uint8_t *ui, size_t ui_cap); + +const char *hybbx_circuit_proto_name(hybbx_circuit_proto_t proto); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CIRCUIT_H */ diff --git a/include/hybbx/circuit_balance.h b/include/hybbx/circuit_balance.h new file mode 100644 index 0000000..80edbb0 --- /dev/null +++ b/include/hybbx/circuit_balance.h @@ -0,0 +1,131 @@ +#ifndef HYBBX_CIRCUIT_BALANCE_H +#define HYBBX_CIRCUIT_BALANCE_H + +/** + * HBX circuit auto load-balancing — pace and stabilize mixed link speeds, + * duplex modes, and sync/async downlink traffic toward edge adapters. + * + * Escalation (low-bandwidth links): pause → break → cancel connection. + * A few seconds of lag on slow links is normal; the balancer auto-stabilizes. + */ + +#include "hybbx/circuit.h" +#include "hybbx/link.h" +#include "hybbx/types.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_CIRCUIT_BALANCE_QUEUE_MAX 64u +#define HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD 115200u +#define HYBBX_CIRCUIT_BALANCE_LOW_BAUD_THRESHOLD 9600u + +typedef enum hybbx_circuit_bandwidth { + HYBBX_CIRCUIT_BW_HIGH = 0, + HYBBX_CIRCUIT_BW_LOW = 1 +} hybbx_circuit_bandwidth_t; + +typedef enum hybbx_circuit_duplex { + HYBBX_CIRCUIT_DUPLEX_UNSET = 0, + HYBBX_CIRCUIT_DUPLEX_HALF = 1, + HYBBX_CIRCUIT_DUPLEX_FULL = 2 +} hybbx_circuit_duplex_t; + +typedef enum hybbx_circuit_balance_action { + HYBBX_CIRCUIT_BAL_NONE = 0, + HYBBX_CIRCUIT_BAL_PAUSE = 1, + HYBBX_CIRCUIT_BAL_BREAK = 2, + HYBBX_CIRCUIT_BAL_CANCEL = 3, + HYBBX_CIRCUIT_BAL_RESUME = 4 +} hybbx_circuit_balance_action_t; + +typedef enum hybbx_circuit_balance_tick_result { + HYBBX_CIRCUIT_BAL_TICK_OK = 0, + HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK = 1 +} hybbx_circuit_balance_tick_result_t; + +typedef struct hybbx_circuit_balance_config { + int enabled; + unsigned lag_sec; + size_t queue_pause; + size_t queue_break; + size_t queue_cancel; +} hybbx_circuit_balance_config_t; + +typedef struct hybbx_circuit_link_profile { + hybbx_circuit_bandwidth_t bandwidth; + hybbx_circuit_duplex_t duplex; + unsigned baud; + double frequency_mhz; +} hybbx_circuit_link_profile_t; + +typedef struct hybbx_circuit_link_qos { + unsigned baud; + int duplex; + const char *bandwidth; + const char *frequency_mhz; +} hybbx_circuit_link_qos_t; + +typedef hybbx_result_t (*hybbx_circuit_balance_send_fn)(void *ctx, + const uint8_t *frame, + size_t len); + +typedef void (*hybbx_circuit_balance_flow_fn)(void *ctx, + hybbx_circuit_balance_action_t action, + const char *reason); + +typedef struct hybbx_circuit_balance hybbx_circuit_balance_t; + +void hybbx_circuit_balance_config_defaults(hybbx_circuit_balance_config_t *cfg); + +void hybbx_circuit_link_profile_from_auth(const hybbx_link_auth_t *auth, + hybbx_circuit_link_profile_t *out); + +const char *hybbx_circuit_balance_action_name(hybbx_circuit_balance_action_t action); + +hybbx_result_t hybbx_circuit_flow_ctrl_parse(const char *payload, size_t len, + hybbx_circuit_balance_action_t *action_out, + char *reason_out, size_t reason_cap); + +size_t hybbx_circuit_flow_ctrl_format(hybbx_circuit_balance_action_t action, + const char *reason, + char *out, size_t out_cap); + +hybbx_circuit_balance_t *hybbx_circuit_balance_create( + const hybbx_circuit_balance_config_t *cfg); + +void hybbx_circuit_balance_destroy(hybbx_circuit_balance_t *bal); + +void hybbx_circuit_balance_set_profile(hybbx_circuit_balance_t *bal, + const hybbx_circuit_link_profile_t *profile); + +hybbx_circuit_balance_action_t hybbx_circuit_balance_action( + const hybbx_circuit_balance_t *bal); + +size_t hybbx_circuit_balance_queued_bytes(const hybbx_circuit_balance_t *bal); + +/** De-escalate from cancel after user sacrifice — keep secondary link up. */ +void hybbx_circuit_balance_spared_cancel(hybbx_circuit_balance_t *bal); + +hybbx_result_t hybbx_circuit_balance_submit(hybbx_circuit_balance_t *bal, + const uint8_t *frame, size_t len, + hybbx_circuit_balance_send_fn send_fn, + void *send_ctx, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx); + +hybbx_circuit_balance_tick_result_t hybbx_circuit_balance_tick( + hybbx_circuit_balance_t *bal, unsigned poll_ms, + hybbx_circuit_balance_send_fn send_fn, void *send_ctx, + hybbx_circuit_balance_flow_fn flow_fn, void *flow_ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CIRCUIT_BALANCE_H */ diff --git a/include/hybbx/circuit_bridge.h b/include/hybbx/circuit_bridge.h new file mode 100644 index 0000000..1eb24b4 --- /dev/null +++ b/include/hybbx/circuit_bridge.h @@ -0,0 +1,46 @@ +#ifndef HYBBX_CIRCUIT_BRIDGE_H +#define HYBBX_CIRCUIT_BRIDGE_H + +/** + * Main-side bridge registry for remote Secondaries — one entry per + * [transport.packet_radioN], [transport.ardopN], or [transport.crdopN] section + * (link metadata; RF runs on the Secondary host). + * Secondaries (edge extenders/repeaters, not telnet users or local Main transports) + * authenticate with matching link_id + link_password over the HBX/TCP circuit hub. + */ + +#include "hybbx/config.h" +#include "hybbx/link.h" +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_circuit_bridge_entry { + char link_id[HYBBX_LINK_ID_MAX]; + char link_password[128]; + char link_role[HYBBX_LINK_ROLE_MAX]; + double frequency_mhz; +} hybbx_circuit_bridge_entry_t; + +typedef struct hybbx_circuit_bridge_registry { + hybbx_circuit_bridge_entry_t entries[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned count; +} hybbx_circuit_bridge_registry_t; + +void hybbx_circuit_bridge_clear(hybbx_circuit_bridge_registry_t *reg); + +/** Load transport.packet_radioN, transport.ardopN, and transport.crdopN sections. */ +hybbx_result_t hybbx_circuit_bridge_load(hybbx_circuit_bridge_registry_t *reg, + const hybbx_config_t *config); + +const hybbx_circuit_bridge_entry_t *hybbx_circuit_bridge_find( + const hybbx_circuit_bridge_registry_t *reg, const char *link_id); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CIRCUIT_BRIDGE_H */ diff --git a/include/hybbx/circuit_tcp.h b/include/hybbx/circuit_tcp.h new file mode 100644 index 0000000..900c379 --- /dev/null +++ b/include/hybbx/circuit_tcp.h @@ -0,0 +1,162 @@ +#ifndef HYBBX_CIRCUIT_TCP_H +#define HYBBX_CIRCUIT_TCP_H + +/** + * HBX/Circuit TCP hub on Main (Circuit = TCP listener :7323; HBX = framed + * payloads on that stream). Remote Secondary processes connect via LINK_AUTH. + * Secondaries are separate edge machines (extenders/repeaters), not telnet users + * or local [transport.*] adapters on Main. + * Hub: [circuit] bind/port. Client: hybbx_circuit_link_* + LINK_AUTH. + * All inter-node paths (Secondary, mains_proxy mesh, future relays) use this + * client pattern only — never a direct socket to a remote HyBBX Main. + * Formal name: HBX — Hybrid Bridge eXchange (v1); see docs/TOPOLOGY.md. + */ + +#include "hybbx/circuit.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/circuit_bridge.h" +#include "hybbx/service.h" +#include "hybbx/types.h" +#include "hybbx/limits.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_circuit_hub hybbx_circuit_hub_t; + +typedef struct hybbx_circuit_config { + char bind4[64]; + char bind6[64]; + unsigned port; + int ipv4; + int ipv6; + char link_password[128]; + unsigned link_stale_days; + int link_auth; + char data_path[HYBBX_PATH_MAX]; + char config_path[HYBBX_PATH_MAX]; + unsigned max_links; + hybbx_circuit_bridge_registry_t bridge; + hybbx_circuit_balance_config_t balance; +} hybbx_circuit_config_t; + +/** Internal transport plugin used for sessions bridged over the circuit hub. */ +extern const hybbx_transport_plugin_t hybbx_plugin_circuit; + +void hybbx_circuit_config_defaults(hybbx_circuit_config_t *cfg); + +hybbx_circuit_hub_t *hybbx_circuit_hub_create(hybbx_service_t *service); +void hybbx_circuit_hub_destroy(hybbx_circuit_hub_t *hub); + +hybbx_result_t hybbx_circuit_hub_start(hybbx_circuit_hub_t *hub, + const hybbx_circuit_config_t *cfg); + +void hybbx_circuit_hub_stop(hybbx_circuit_hub_t *hub); + +int hybbx_circuit_hub_running(const hybbx_circuit_hub_t *hub); + +unsigned hybbx_circuit_hub_port(const hybbx_circuit_hub_t *hub); + +/** + * Send a pre-encoded HBX frame to the attached link adapter. + */ +hybbx_result_t hybbx_circuit_hub_send_raw(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len); + +hybbx_result_t hybbx_circuit_hub_send_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len); + +unsigned hybbx_circuit_hub_active_link_count(const hybbx_circuit_hub_t *hub); + +/** + * Send HBX to all active links. @p frequency_mhz 0 = any MHz. + * @p require_broadcast_qos filters to low-bandwidth + half-duplex links. + * Returns OK when at least one link accepted the frame. + */ +hybbx_result_t hybbx_circuit_hub_multicast_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len, + double frequency_mhz, + int require_broadcast_qos, + unsigned *sent_out); + +/** + * Send HBX to one circuit slot (sequential AX.25 broadcast). + * @p slot_index must be an active broadcast-QoS link when @p require_broadcast_qos. + */ +hybbx_result_t hybbx_circuit_hub_send_hbx_slot(hybbx_circuit_hub_t *hub, + unsigned slot_index, + const uint8_t *frame, size_t len, + int require_broadcast_qos); + +double hybbx_circuit_hub_link_frequency_mhz(const hybbx_circuit_hub_t *hub); + +/** Non-zero when link QoS is low-bandwidth and half-duplex (AX.25 broadcast allowed). */ +int hybbx_circuit_hub_link_broadcast_qos(const hybbx_circuit_hub_t *hub); + +typedef struct hybbx_circuit_broadcast_link { + double frequency_mhz; + unsigned slot_index; + char link_id[HYBBX_LINK_ID_MAX]; +} hybbx_circuit_broadcast_link_t; + +/** + * Active broadcast-QoS links sorted by MHz (then link_id). + * Returns count written to @p out (may be 0). + */ +unsigned hybbx_circuit_hub_broadcast_links(const hybbx_circuit_hub_t *hub, + hybbx_circuit_broadcast_link_t *out, + unsigned out_max); + +void hybbx_circuit_hub_prune_links(hybbx_circuit_hub_t *hub); + +/** Record RF activity on a link adapter (RX or TX). */ +void hybbx_circuit_hub_note_rf_activity(hybbx_circuit_hub_t *hub, + const char *link_id); + +/** Non-zero when @p slot_index has had no RF activity for @p min_idle_sec. */ +int hybbx_circuit_hub_link_band_idle(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec); + +/** + * Earliest time a broadcast may TX on @p slot_index (now if already idle). + * Returns (time_t)-1 when the slot is unavailable. + */ +time_t hybbx_circuit_hub_link_band_ready_at(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec); + +/** Link adapter: connect to the internal circuit hub (TCP client). */ +hybbx_result_t hybbx_circuit_link_connect(const char *host, unsigned port, + int *out_fd); + +hybbx_result_t hybbx_circuit_link_write(int fd, const uint8_t *frame, + size_t len); + +hybbx_result_t hybbx_circuit_link_read(int fd, uint8_t *buf, size_t buf_len, + size_t *read_len); + +/** + * Send LINK_AUTH HBX frame after TCP connect (link/repeater edge daemon + * toward the centralized daemon). Password only — no heartbeat/ping. + */ +hybbx_result_t hybbx_circuit_link_authenticate(int fd, + const char *password, + const char *role, + const char *id); + +/** + * LINK_AUTH with optional QoS (baud, duplex, bandwidth) for auto load-balancing. + */ +hybbx_result_t hybbx_circuit_link_authenticate_ex(int fd, + const char *password, + const char *role, + const char *id, + const hybbx_circuit_link_qos_t *qos); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CIRCUIT_TCP_H */ diff --git a/include/hybbx/command.h b/include/hybbx/command.h new file mode 100644 index 0000000..5d8ba5c --- /dev/null +++ b/include/hybbx/command.h @@ -0,0 +1,71 @@ +#ifndef HYBBX_COMMAND_H +#define HYBBX_COMMAND_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; +struct hybbx_session; + +/** + * Input line routing scope. + * + * HyBBX accepts only lines starting with '/'. Valid forms: + * / or /command /cmd /commands → /help + * / or / → command + * /command or /cmd → same as / + * + * - COMMENT: starts with ';' or '#' — ignored (like empty line) + * - LOCAL: anything else — not HyBBX; silently ignored + */ +typedef enum hybbx_command_scope { + HYBBX_CMD_SCOPE_HYBBX = 1, + HYBBX_CMD_SCOPE_LOCAL = 2, + HYBBX_CMD_SCOPE_COMMENT = 3 +} hybbx_command_scope_t; + +typedef struct hybbx_parsed_command { + hybbx_command_scope_t scope; + char *line; + char *verb; + char **argv; + size_t argc; +} hybbx_parsed_command_t; + +/** + * Classify a single input line. + * Returns HYBBX_CMD_SCOPE_HYBBX only when the first non-whitespace + * character is '/'. + */ +hybbx_command_scope_t hybbx_command_classify(const char *line); + +/** Non-zero when @p line is a HyBBX system command (starts with '/'). */ +int hybbx_command_is_hybbx(const char *line); + +/** Non-zero when @p line is a local comment (';' or '#' after whitespace). */ +int hybbx_command_is_comment(const char *line); + +/** + * Parse a trimmed command line into verb and arguments. + * For HyBBX commands the leading '/' is stripped from the verb. + */ +hybbx_result_t hybbx_command_parse(const char *line, + hybbx_parsed_command_t *out); + +void hybbx_command_free(hybbx_parsed_command_t *cmd); + +/** + * Dispatch a parsed HyBBX command (scope must be HYBBX_CMD_SCOPE_HYBBX). + */ +hybbx_result_t hybbx_command_dispatch(struct hybbx_service *service, + struct hybbx_session *session, + const hybbx_parsed_command_t *cmd); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_COMMAND_H */ diff --git a/include/hybbx/commands_registry.h b/include/hybbx/commands_registry.h new file mode 100644 index 0000000..22b1143 --- /dev/null +++ b/include/hybbx/commands_registry.h @@ -0,0 +1,73 @@ +#ifndef HYBBX_COMMANDS_REGISTRY_H +#define HYBBX_COMMANDS_REGISTRY_H + +#include "hybbx/auth.h" +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_session; + +typedef struct hybbx_command_def { + char verb[HYBBX_CMD_VERB_MAX]; + char group[16]; + hybbx_user_level_t min_level; + hybbx_user_level_t max_level; /* 0 = no upper cap */ + int only_level; /* HYBBX_LEVEL_* when set; else 0 */ + char line1[HYBBX_COMMANDS_HELP_LINE_MAX]; + char line2[HYBBX_COMMANDS_HELP_LINE_MAX]; +} hybbx_command_def_t; + +/** Load areas.yaml then commands.yaml from install root. Call once at service startup. */ +hybbx_result_t hybbx_commands_registry_init(void); + +void hybbx_commands_registry_shutdown(void); + +const hybbx_command_def_t *hybbx_commands_registry_find(const char *verb); + +/** Map alias or topic name to canonical verb; returns @p topic if unknown. */ +const char *hybbx_commands_registry_canonical(const char *topic); + +int hybbx_commands_registry_verb_allowed(hybbx_user_level_t level, + const char *verb); + +int hybbx_commands_registry_help_allowed(hybbx_user_level_t level, + const char *verb); + +int hybbx_commands_registry_may_userchange(hybbx_user_level_t actor, + hybbx_user_level_t target); + +int hybbx_commands_registry_may_userdelete(hybbx_user_level_t actor, + hybbx_user_level_t target); + +int hybbx_commands_registry_may_promote(hybbx_user_level_t actor, + hybbx_user_level_t target, + int target_active, + hybbx_user_level_t new_level); + +int hybbx_commands_registry_may_demote(hybbx_user_level_t actor, + hybbx_user_level_t target); + +int hybbx_commands_registry_may_delete(hybbx_user_level_t actor, + hybbx_user_level_t target); + +void hybbx_commands_registry_show_menu(struct hybbx_session *session); +void hybbx_commands_registry_show_index(struct hybbx_session *session); +void hybbx_commands_registry_show_aliases(struct hybbx_session *session); +void hybbx_commands_registry_show_help(struct hybbx_session *session, + const char *canonical); + +/** + * List line1 for every registry command with min Admin or Sysop + * (the admin set a Sysop may run while staying invisible via /monitor). + */ +void hybbx_commands_registry_show_sysop_cmds(struct hybbx_session *session); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_COMMANDS_REGISTRY_H */ diff --git a/include/hybbx/conference.h b/include/hybbx/conference.h new file mode 100644 index 0000000..53baa27 --- /dev/null +++ b/include/hybbx/conference.h @@ -0,0 +1,69 @@ +#ifndef HYBBX_CONFERENCE_H +#define HYBBX_CONFERENCE_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; +struct hybbx_session; + +#define HYBBX_CONFERENCE_TOPIC_MAX 32 + +/** Invite rate limit: max invites per target user within the window. */ +#define HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET 2u + +/** Invite rate limit window (seconds). */ +#define HYBBX_CONFERENCE_INVITE_WINDOW_SEC 1800u + +/** + * Send a conference invite: @p topic to @p partner (login or nickname). + * Partner must accept with y/n or yes/no. Max two invites per target per 30 min. + */ +hybbx_result_t hybbx_conference_start(struct hybbx_service *service, + struct hybbx_session *initiator, + const char *topic, + const char *partner); + +/** + * Monitor/sysop meet: invite @p partner with timeout, or @p force join immediately. + * Topic defaults to "monitor". Status lines go to initiator. + */ +hybbx_result_t hybbx_conference_monitor_meet(struct hybbx_service *service, + struct hybbx_session *initiator, + const char *partner, + int force); + +/** Expire timed-out invites (monitor path). Safe to call every session tick. */ +void hybbx_conference_invite_tick(struct hybbx_service *service, + struct hybbx_session *session); + +/** Non-zero when @p session has a pending conference invite to answer. */ +int hybbx_conference_invite_pending(const struct hybbx_session *session); + +/** + * Handle invite reply line (y/n, yes/no). Call for local input while invite pending. + * Returns HYBBX_OK when consumed. + */ +hybbx_result_t hybbx_conference_reply_invite(struct hybbx_service *service, + struct hybbx_session *session, + const char *line); + +/** Clear pending invites involving @p session (disconnect). */ +void hybbx_conference_session_closed(struct hybbx_session *session); + +/** Deliver a line within an active conference (sender must be in conference area). */ +hybbx_result_t hybbx_conference_post(struct hybbx_service *service, + struct hybbx_session *from, + const char *message); + +/** Tear down conference state when leaving the conference area. */ +void hybbx_conference_area_leaving(struct hybbx_session *session); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CONFERENCE_H */ diff --git a/include/hybbx/config.h b/include/hybbx/config.h new file mode 100644 index 0000000..c2083ab --- /dev/null +++ b/include/hybbx/config.h @@ -0,0 +1,126 @@ +#ifndef HYBBX_CONFIG_H +#define HYBBX_CONFIG_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_config_entry { + char *section; + char *key; + char *value; +} hybbx_config_entry_t; + +typedef struct hybbx_config { + hybbx_config_entry_t *entries; + size_t count; +} hybbx_config_t; + +/** Load configuration from an INI file. */ +hybbx_result_t hybbx_config_load(hybbx_config_t *config, const char *path); + +/** Release all memory held by @p config. */ +void hybbx_config_free(hybbx_config_t *config); + +/** + * Return the value for @p key in @p section, or @p default_value if missing. + * Both @p section and @p key are case-sensitive. + */ +const char *hybbx_config_get(const hybbx_config_t *config, + const char *section, + const char *key, + const char *default_value); + +/** Parse HyBBX boolean tokens (yes/no and aliases; see hybbx_parse_bool). */ +int hybbx_config_get_bool(const hybbx_config_t *config, + const char *section, + const char *key, + int default_value); + +/** + * Parse an unsigned decimal integer in [@p min_value, @p max_value]. + * Returns @p default_value when missing or invalid. + */ +unsigned hybbx_config_get_uint(const hybbx_config_t *config, + const char *section, + const char *key, + unsigned default_value, + unsigned min_value, + unsigned max_value); + +/** + * Build a semicolon-separated key=value string for all keys in @p section. + * Caller must free the returned string. + */ +char *hybbx_config_format_section(const hybbx_config_t *config, + const char *section); + +/** + * Format all @c transport.<plugin> and @c transport.<plugin>N + * sections (in file order) into one start string. Multiple sections are + * joined with @ref HYBBX_PACKET_RADIO_INSTANCE_SEP (packet_radio only). + * Caller must free the returned string. + */ +char *hybbx_config_format_transport_sections(const hybbx_config_t *config, + const char *plugin_name); + +/** + * Like @ref hybbx_config_format_transport_sections for @c packet_radio, but + * prepends a `[max25]` key chunk when that INI section exists. + */ +char *hybbx_config_format_packet_radio_start(const hybbx_config_t *config); + +/** Like packet_radio start — prepends `[max25]` keys for baycom transport. */ +char *hybbx_config_format_baycom_start(const hybbx_config_t *config); + +/** + * Prepend @c [max25] keys as a packet_radio start prefix (before instance sep). + * Caller must free the returned string. + */ +char *hybbx_config_prepend_packet_radio_max25(const hybbx_config_t *config, + const char *body); + +/** + * Resolve INI section for a transport plugin: @c transport.<plugin> first, + * else the first @c transport.<plugin><digits> section with keys + * (e.g. @c transport.packet_radio1). Writes the chosen name to @p out_section. + * Returns 1 when a numbered (or legacy) section with keys was found, else 0. + */ +int hybbx_config_resolve_transport_section(const hybbx_config_t *config, + const char *plugin_name, + char *out_section, + size_t out_size); + +typedef void (*hybbx_config_iter_fn)(const char *section, const char *key, + const char *value, void *ctx); + +typedef void (*hybbx_config_section_iter_fn)(const char *section, void *ctx); + +/** Invoke @p fn for every entry in @p config (in file order). */ +void hybbx_config_foreach(const hybbx_config_t *config, + hybbx_config_iter_fn fn, void *ctx); + +/** Invoke @p fn once per unique section name (first occurrence order). */ +void hybbx_config_foreach_section(const hybbx_config_t *config, + hybbx_config_section_iter_fn fn, void *ctx); + +/** Set or replace a key in @p config (in-memory only until saved). */ +hybbx_result_t hybbx_config_set(hybbx_config_t *config, + const char *section, + const char *key, + const char *value); + +/** Remove all keys in @p section from @p config. */ +void hybbx_config_remove_section(hybbx_config_t *config, const char *section); + +/** Write @p config to @p path (INI format). */ +hybbx_result_t hybbx_config_save(const hybbx_config_t *config, + const char *path); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CONFIG_H */ diff --git a/include/hybbx/crdop.h b/include/hybbx/crdop.h new file mode 100644 index 0000000..be98200 --- /dev/null +++ b/include/hybbx/crdop.h @@ -0,0 +1,44 @@ +#ifndef HYBBX_CRDOP_H +#define HYBBX_CRDOP_H + +/** + * CRDOP — CB Radio Digital Open Protocol (Level 2, experimental). + * + * HyBBX `crdop` plugin: CB host-client bridge to external CRDOPC. + * ARDOP uses the `ardop` plugin with ARDOPC/ardopcf. + * Modem DSP stays outside HyBBX. Profile helpers shared with `ardop`. + */ + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum hybbx_crdop_radio_profile { + HYBBX_CRDOP_PROFILE_AMATEUR = 0, + HYBBX_CRDOP_PROFILE_CB = 1 +} hybbx_crdop_radio_profile_t; + +/** Parse radio_profile INI value (cb, amateur). */ +hybbx_crdop_radio_profile_t hybbx_crdop_profile_parse(const char *value); + +const char *hybbx_crdop_profile_name(hybbx_crdop_radio_profile_t profile); + +/** Default ARQ bandwidth string for profile when INI omits arq_bandwidth. */ +const char *hybbx_crdop_default_arq_bandwidth(hybbx_crdop_radio_profile_t profile); + +/** Non-zero when bandwidth string is above CB-safe ceiling (1000MAX). */ +int hybbx_crdop_bandwidth_exceeds_cb(const char *arq_bandwidth); + +struct hybbx_ardop_config; + +/** Parse `[transport.crdop]` config (CB defaults). */ +hybbx_result_t hybbx_crdop_config_parse(const char *config, + struct hybbx_ardop_config *out); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CRDOP_H */ diff --git a/include/hybbx/crypto.h b/include/hybbx/crypto.h new file mode 100644 index 0000000..5411e5e --- /dev/null +++ b/include/hybbx/crypto.h @@ -0,0 +1,101 @@ +#ifndef HYBBX_CRYPTO_H +#define HYBBX_CRYPTO_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * HyBBX reversible cryptography. + * + * Defaults use bundled in-tree sources (tinyaes, monocypher). Backends are + * selectable system-wide via `[crypto]` in hybbx.ini when optional OpenSSL or + * libsodium support is enabled at build time (see hybbx/crypto_config.h). + * + * Three modern AEAD schemes: + * 1. AES-256-GCM (NIST, TLS, IPsec) + * 2. XChaCha20-Poly1305 (extended-nonce ChaCha, Monocypher) + * 3. X25519 + XChaCha20-Poly1305 sealed messages (ECIES-style, Monocypher) + * + * Password hashing (one-way) remains in hybbx/password.h (SHA-256 / MD5). + */ + +typedef enum hybbx_crypto_alg { + HYBBX_CRYPTO_AES_256_GCM = 1, + HYBBX_CRYPTO_XCHACHA20_POLY1305 = 2, + HYBBX_CRYPTO_X25519_AEAD = 3 +} hybbx_crypto_alg_t; + +#define HYBBX_CRYPTO_KEY_SIZE 32 +#define HYBBX_CRYPTO_TAG_SIZE 16 +#define HYBBX_CRYPTO_AES_GCM_NONCE 12 +#define HYBBX_CRYPTO_XCHACHA_NONCE 24 +#define HYBBX_CRYPTO_X25519_PUBLIC_KEY 32 +#define HYBBX_CRYPTO_X25519_SECRET_KEY 32 +/** Sealed blob overhead: ephemeral_pk(32) + nonce(24) + tag(16). */ +#define HYBBX_CRYPTO_X25519_SEAL_OVERHEAD 72 + +/** Fill @p buf with system random bytes. */ +hybbx_result_t hybbx_crypto_random(uint8_t *buf, size_t len); + +/** Human-readable cipher name. */ +const char *hybbx_crypto_alg_name(hybbx_crypto_alg_t alg); + +/** Required nonce length for @p alg (0 for X25519 sealed mode). */ +size_t hybbx_crypto_nonce_size(hybbx_crypto_alg_t alg); + +/** + * Symmetric AEAD encrypt (algorithms 1 and 2). + * @p ciphertext length equals @p plaintext_len; authentication tag is separate. + */ +hybbx_result_t hybbx_crypto_encrypt(hybbx_crypto_alg_t alg, + const uint8_t key[HYBBX_CRYPTO_KEY_SIZE], + const uint8_t *nonce, size_t nonce_len, + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]); + +/** Symmetric AEAD decrypt; returns HYBBX_ERR_DENIED when the tag is invalid. */ +hybbx_result_t hybbx_crypto_decrypt(hybbx_crypto_alg_t alg, + const uint8_t key[HYBBX_CRYPTO_KEY_SIZE], + const uint8_t *nonce, size_t nonce_len, + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + const uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]); + +/** Generate an X25519 key pair (Montgomery curve, RFC 7748). */ +hybbx_result_t hybbx_crypto_x25519_keypair( + uint8_t public_key[HYBBX_CRYPTO_X25519_PUBLIC_KEY], + uint8_t secret_key[HYBBX_CRYPTO_X25519_SECRET_KEY]); + +/** + * Asymmetric seal for @p recipient_pk. + * Output: ephemeral_pk || nonce || ciphertext || tag. + */ +hybbx_result_t hybbx_crypto_x25519_seal( + const uint8_t recipient_pk[HYBBX_CRYPTO_X25519_PUBLIC_KEY], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *sealed, size_t *sealed_len, size_t sealed_cap); + +/** Open a sealed message with @p recipient_sk. */ +hybbx_result_t hybbx_crypto_x25519_open( + const uint8_t recipient_sk[HYBBX_CRYPTO_X25519_SECRET_KEY], + const uint8_t *aad, size_t aad_len, + const uint8_t *sealed, size_t sealed_len, + uint8_t *plaintext, size_t *plaintext_len, size_t plaintext_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CRYPTO_H */ diff --git a/include/hybbx/crypto_config.h b/include/hybbx/crypto_config.h new file mode 100644 index 0000000..ec1f61d --- /dev/null +++ b/include/hybbx/crypto_config.h @@ -0,0 +1,69 @@ +#ifndef HYBBX_CRYPTO_CONFIG_H +#define HYBBX_CRYPTO_CONFIG_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_config; + +/** Password hashing backend for new hashes and verification. */ +typedef enum hybbx_password_hash_backend { + HYBBX_PASSWORD_HASH_TINYSHA256 = 1, + HYBBX_PASSWORD_HASH_OPENSSL = 2 +} hybbx_password_hash_backend_t; + +/** AES-256-GCM implementation backend. */ +typedef enum hybbx_aes_gcm_backend { + HYBBX_AES_GCM_TINYAES = 1, + HYBBX_AES_GCM_OPENSSL = 2 +} hybbx_aes_gcm_backend_t; + +/** XChaCha20-Poly1305 implementation backend. */ +typedef enum hybbx_chacha_backend { + HYBBX_CHACHA_MONOCYPHER = 1, + HYBBX_CHACHA_LIBSODIUM = 2 +} hybbx_chacha_backend_t; + +/** X25519 key agreement backend (sealed-box mode). */ +typedef enum hybbx_x25519_backend { + HYBBX_X25519_MONOCYPHER = 1, + HYBBX_X25519_LIBSODIUM = 2 +} hybbx_x25519_backend_t; + +/** OS random bytes source. */ +typedef enum hybbx_random_backend { + HYBBX_RANDOM_SYSTEM = 1, + HYBBX_RANDOM_OPENSSL = 2 +} hybbx_random_backend_t; + +typedef struct hybbx_crypto_config { + hybbx_password_hash_backend_t password_hash; + hybbx_aes_gcm_backend_t aes_gcm; + hybbx_chacha_backend_t chacha; + hybbx_x25519_backend_t x25519; + hybbx_random_backend_t random; +} hybbx_crypto_config_t; + +/** Bundled defaults: tinysha256, tinyaes, monocypher, system random. */ +void hybbx_crypto_config_defaults(hybbx_crypto_config_t *cfg); + +/** Load `[crypto]` from @p config (missing keys keep defaults). */ +void hybbx_crypto_config_apply(const struct hybbx_config *config); + +/** Active system-wide crypto settings (defaults until apply is called). */ +const hybbx_crypto_config_t *hybbx_crypto_config_get(void); + +const char *hybbx_password_hash_backend_name(hybbx_password_hash_backend_t b); +const char *hybbx_aes_gcm_backend_name(hybbx_aes_gcm_backend_t b); +const char *hybbx_chacha_backend_name(hybbx_chacha_backend_t b); +const char *hybbx_x25519_backend_name(hybbx_x25519_backend_t b); +const char *hybbx_random_backend_name(hybbx_random_backend_t b); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CRYPTO_CONFIG_H */ diff --git a/include/hybbx/daemon_wrap.h b/include/hybbx/daemon_wrap.h new file mode 100644 index 0000000..0144e5c --- /dev/null +++ b/include/hybbx/daemon_wrap.h @@ -0,0 +1,36 @@ +#ifndef HYBBX_DAEMON_WRAP_H +#define HYBBX_DAEMON_WRAP_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_DAEMON_DEFAULT_SESSION "hybbxd" +#define HYBBX_DAEMON_WRAP_ENV "HYBBXD_WRAPPED" + +typedef struct hybbx_daemon_launch_opts { + int foreground; + int attach; + int use_screen; + int use_tmux; + char session[32]; +} hybbx_daemon_launch_opts_t; + +void hybbx_daemon_launch_opts_defaults(hybbx_daemon_launch_opts_t *opts); + +/** + * Handle --screen / --tmux / --attach before the service loop. + * Spawns or attaches via GNU screen or tmux when requested. + * Returns HYBBX_OK to continue a foreground run; otherwise exits the process. + */ +hybbx_result_t hybbx_daemon_apply_launch_opts(const hybbx_daemon_launch_opts_t *opts, + const char *binary_path, + int argc, char **argv); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_DAEMON_WRAP_H */ diff --git a/include/hybbx/hybbx.h b/include/hybbx/hybbx.h new file mode 100644 index 0000000..e086c2a --- /dev/null +++ b/include/hybbx/hybbx.h @@ -0,0 +1,10 @@ +#ifndef HYBBX_H +#define HYBBX_H + +#define HYBBX_VERSION_MAJOR 2 +#define HYBBX_VERSION_MINOR 8 +#define HYBBX_VERSION_PATCH 0 + +#define HYBBX_VERSION_STRING "2.8.0" + +#endif /* HYBBX_H */ diff --git a/include/hybbx/instance.h b/include/hybbx/instance.h new file mode 100644 index 0000000..9ed07d3 --- /dev/null +++ b/include/hybbx/instance.h @@ -0,0 +1,61 @@ +#ifndef HYBBX_INSTANCE_H +#define HYBBX_INSTANCE_H + +#include "hybbx/networks.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Hardcoded network layout instance (compile-time per binary). + * + * hybbxd — Main (local BBX; WebSocket user path; hub + mains_proxy) + * hybbxsd — Secondary (other-Main peers via mains_proxy; no user BBX) + * hybbxpd — Proxy (hybrid bridge; no WebSocket; no in-process AX.25) + * + * SSoT: vault projects/hybbx/2026-07-16-network-layout-instances.md + */ +typedef enum hybbx_instance_role { + HYBBX_INSTANCE_MAIN = 1, + HYBBX_INSTANCE_SECONDARY = 2, + HYBBX_INSTANCE_PROXY = 3 +} hybbx_instance_role_t; + +hybbx_instance_role_t hybbx_instance_role(void); + +/** + * Standalone Main (hybbxd + local RF on one host). + * Set from INI `[instance] standalone=yes` or auto when Main INI enables local RF. + */ +void hybbx_instance_set_standalone(int enabled); +int hybbx_instance_standalone(void); + +/** "hybbxd" / "hybbxsd" / "hybbxpd" */ +const char *hybbx_instance_binary_name(void); + +/** Short role label: "Main" / "Secondary" / "Proxy". */ +const char *hybbx_instance_role_name(void); + +/** Non-zero when this instance runs local user BBX (login/mail/chat). */ +int hybbx_instance_offers_user_bbx(void); + +/** + * Apply hard layout rules to @p networks (force/clear flags). + * Logs warnings when INI requested a forbidden combination. + * Returns HYBBX_OK, or HYBBX_ERR_INVALID when the role cannot start safely. + */ +hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks); + +/** + * Non-zero when @p plugin_name may be registered/started for this instance. + * Complements [networks] wanted checks. + */ +int hybbx_instance_plugin_allowed(const char *plugin_name); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_INSTANCE_H */ diff --git a/include/hybbx/kiss.h b/include/hybbx/kiss.h new file mode 100644 index 0000000..9cd378d --- /dev/null +++ b/include/hybbx/kiss.h @@ -0,0 +1,61 @@ +#ifndef HYBBX_KISS_H +#define HYBBX_KISS_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_KISS_FEND 0xC0u +#define HYBBX_KISS_FESC 0xDBu +#define HYBBX_KISS_TFEND 0xDCu +#define HYBBX_KISS_TFESC 0xDDu + +typedef enum hybbx_kiss_command { + HYBBX_KISS_CMD_DATA = 0x00, + HYBBX_KISS_CMD_TXDELAY = 0x01, + HYBBX_KISS_CMD_PERSIST = 0x02, + HYBBX_KISS_CMD_SLOTTIME = 0x03, + HYBBX_KISS_CMD_TXTAIL = 0x04, + HYBBX_KISS_CMD_FULLDUPLEX = 0x05, + HYBBX_KISS_CMD_SETHARDWARE = 0x06 +} hybbx_kiss_command_t; + +#define HYBBX_KISS_MAX_FRAME 2048 + +typedef void (*hybbx_kiss_frame_cb)(uint8_t port, const uint8_t *frame, + size_t len, void *userdata); + +typedef struct hybbx_kiss_decoder { + uint8_t buf[HYBBX_KISS_MAX_FRAME]; + size_t len; + int in_frame; + int escape; +} hybbx_kiss_decoder_t; + +void hybbx_kiss_decoder_init(hybbx_kiss_decoder_t *dec); + +/** + * Feed raw serial bytes; complete KISS frames invoke @p cb. + */ +void hybbx_kiss_decoder_feed(hybbx_kiss_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_kiss_frame_cb cb, void *userdata); + +/** + * Encode a KISS frame into @p out (must hold at least @p out_cap bytes). + * Returns encoded length or 0 on error. + */ +size_t hybbx_kiss_encode(uint8_t port, hybbx_kiss_command_t cmd, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_KISS_H */ diff --git a/include/hybbx/limits.h b/include/hybbx/limits.h new file mode 100644 index 0000000..081af1f --- /dev/null +++ b/include/hybbx/limits.h @@ -0,0 +1,136 @@ +#ifndef HYBBX_LIMITS_H +#define HYBBX_LIMITS_H + +#include "hybbx/traffic.h" + +/** INI parser line buffer size. */ +#define HYBBX_CONFIG_LINE_MAX 512 + +/** Maximum path length for files and directories. */ +#define HYBBX_PATH_MAX 512 + +/** Relative paths under the HyBBX install root (see @ref HYBBX_ENV_ROOT). */ +#define HYBBX_DIR_DATA "data" +#define HYBBX_DIR_TEXT "text" +#define HYBBX_DIR_LOGS "logs" +#define HYBBX_FILE_CONFIG "hybbx.ini" +#define HYBBX_FILE_COMMANDS "commands.yaml" +#define HYBBX_FILE_AREAS "areas.yaml" +#define HYBBX_DAEMON_BINARY "hybbxd" + +/** Command registry (share/commands.yaml + share/areas.yaml). */ +#define HYBBX_COMMANDS_MAX 64u +#define HYBBX_AREAS_MAX 12u +#define HYBBX_AREAS_SUB_MAX 4u +#define HYBBX_COMMANDS_VERBS_PER_GROUP 16u +#define HYBBX_COMMANDS_ALIASES_MAX 48u +#define HYBBX_COMMANDS_ALIAS_PER 8u +#define HYBBX_COMMANDS_ALIAS_LINES 8u +#define HYBBX_MENU_LEVELS_MAX 5u +#define HYBBX_MENU_AREAS_PER_LEVEL 12u +#define HYBBX_RIGHTS_TARGET_RULES_MAX 16u +#define HYBBX_RIGHTS_PROMOTE_RULES_MAX 8u +#define HYBBX_RIGHTS_DEMOTE_RULES_MAX 4u +#define HYBBX_COMMANDS_HELP_LINE_MAX 96u +#define HYBBX_COMMANDS_HEADER_MAX 128u + +/** Environment variable: absolute path to the HyBBX install directory. */ +#define HYBBX_ENV_ROOT "HYBBX_ROOT" + +#define HYBBX_CONFIG_SECTION_MAX 128 +#define HYBBX_CONFIG_KEY_MAX 128 +#define HYBBX_CONFIG_VALUE_MAX 256 + +/** Maximum tokens per HyBBX command line (verb + args). */ +#define HYBBX_CMD_TOKEN_MAX 16 + +#define HYBBX_CMD_VERB_MAX 32 + +/** Maximum registered transport plugins. */ +#define HYBBX_MAX_PLUGINS 16 + +/** Maximum length of the global session prompt (empty = no visible prompt). */ +#define HYBBX_PROMPT_MAX 32 + +/** Maximum single allocation for duplicated strings (bytes). */ +#define HYBBX_ALLOC_MAX (256u * 1024u) + +/** Default maximum simultaneous online sessions (guests included). */ +#define HYBBX_DEFAULT_MAX_ONLINE 35u + +/** Maximum concurrent HBX circuit links (secondaries) on Main. */ +#define HYBBX_CIRCUIT_MAX_LINKS 16u + +/** Default [circuit] max_links when not set in INI. */ +#define HYBBX_CIRCUIT_DEFAULT_MAX_LINKS 8u + +/** Maximum concurrent TNC devices in one packet_radio transport. */ +#define HYBBX_PACKET_RADIO_MAX_INSTANCES 8u + +/** Separates multiple INI sections in one packet_radio start config. */ +#define HYBBX_PACKET_RADIO_INSTANCE_SEP '\x1e' + +/** Maximum concurrent BayCom modems in one baycom transport. */ +#define HYBBX_BAYCOM_MAX_INSTANCES 4u + +/** Separates multiple INI sections in one baycom start config. */ +#define HYBBX_BAYCOM_INSTANCE_SEP '\x1e' + +/** Separates multiple INI sections in one mains_proxy start config. */ +#define HYBBX_MAINS_PROXY_INSTANCE_SEP '\x1e' + +/** Default guest session lifetime before auto-disconnect (minutes). */ +#define HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES 30u + +/** Command history depth for interactive line editors. */ +#define HYBBX_HISTORY_MAX 25u + +/** AX.25 auto-beacon minimum interval (seconds); INI may only increase this. */ +#define HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC 900u + +/** Per-link minimum between any AX.25 broadcast TX (manual or auto). */ +#define HYBBX_BROADCAST_AX25_LINK_MIN_SEC 900u + +/** RF channel must be idle this long before an auto AX.25 beacon may TX. */ +#define HYBBX_BROADCAST_AX25_BAND_IDLE_SEC 180u + +/** Minimum pause between successive link TX in one broadcast (seconds). */ +#define HYBBX_BROADCAST_AX25_LINK_GAP_SEC 180u + +/** Retry delay when a sequential broadcast step is deferred (band busy, etc.). */ +#define HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC 30u + +/** Default max25d TCP listen port (MAX25 max25d.ini.example). */ +#define HYBBX_MAX25_DEFAULT_PORT 7325u + +/** Default TCP connect timeout for max25d reachability probe (ms). */ +#define HYBBX_MAX25_PROBE_TIMEOUT_MS 3000u + +/** Wait up to this long for max25d when [max25] check=yes (ms). */ +#define HYBBX_MAX25_PROBE_WAIT_MS 120000u + +/** AX.25 on-air payload cap (1200 baud; keep UI frames short). */ +#define HYBBX_BROADCAST_AX25_MESSAGE_MAX 48u + +/** SQLite fallback copy suffix (see `[storage] backup_path`). */ +#define HYBBX_STORAGE_BACKUP_SUFFIX ".flb" + +/** Default seconds between SQLite DB fallback copies. */ +#define HYBBX_STORAGE_BACKUP_INTERVAL_DEFAULT_SEC 300u + +/** [security] defaults — short cool-down bans (seconds). */ +#define HYBBX_SECURITY_DEFAULT_MAXRETRY 5u +#define HYBBX_SECURITY_DEFAULT_FINDTIME_SEC 600u +#define HYBBX_SECURITY_DEFAULT_BANTIME_SEC 600u +/** Abuse (excessive spam/flood) — higher bar than login failures; no ban for normal use. */ +#define HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY 30u +#define HYBBX_SECURITY_DEFAULT_ABUSE_FINDTIME_SEC 600u +#define HYBBX_SECURITY_DEFAULT_RATE_LIMIT 30u +#define HYBBX_SECURITY_DEFAULT_RATE_WINDOW_SEC 60u +#define HYBBX_SECURITY_BAN_MAX 256u +#define HYBBX_SECURITY_TRACK_MAX 512u + +/** AX.25 CALL-SSID or HBX link_id (same ban table). */ +#define HYBBX_CALLID_MAX 64u + +#endif /* HYBBX_LIMITS_H */ diff --git a/include/hybbx/link.h b/include/hybbx/link.h new file mode 100644 index 0000000..3bd60d1 --- /dev/null +++ b/include/hybbx/link.h @@ -0,0 +1,86 @@ +#ifndef HYBBX_LINK_H +#define HYBBX_LINK_H + +/** + * HyBBX link/repeater edge daemon registry. + * + * Roles: gateway, digipeater, repeater, link — edge daemons toward centralized + * daemon [circuit]. Password auth only (LINK_AUTH); stale prune link_stale_days. + * INI: [circuit] link_* ; data/links/.ini ; [link.] in hybbx.ini. + */ + +#include "hybbx/types.h" +#include "hybbx/limits.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_LINK_STALE_DAYS 10u +#define HYBBX_LINK_ID_MAX 64 +#define HYBBX_LINK_ROLE_MAX 32 +#define HYBBX_LINK_CODE_MAX 16 +#define HYBBX_LINK_AUTH_PAYLOAD_MAX 512 + +typedef struct hybbx_link_auth { + char password[128]; + char role[HYBBX_LINK_ROLE_MAX]; + char id[HYBBX_LINK_ID_MAX]; + unsigned baud; + int duplex; + char bandwidth[16]; + char frequency_mhz[16]; +} hybbx_link_auth_t; + +typedef struct hybbx_link_registry { + char dir[HYBBX_PATH_MAX]; + char config_path[HYBBX_PATH_MAX]; + unsigned stale_days; +} hybbx_link_registry_t; + +void hybbx_link_auth_clear(hybbx_link_auth_t *auth); + +/** + * Build line-oriented LINK_AUTH payload (password=, role=, id=, optional + * baud=, duplex=, bandwidth= for circuit load-balancing). + * @return payload length or 0 on error. + */ +size_t hybbx_link_auth_format(const hybbx_link_auth_t *auth, + char *out, size_t out_cap); + +/** Parse LINK_AUTH payload into @p auth. Returns HYBBX_OK on success. */ +hybbx_result_t hybbx_link_auth_parse(const char *payload, size_t len, + hybbx_link_auth_t *auth); + +void hybbx_link_registry_init(hybbx_link_registry_t *reg, + const char *data_dir, + const char *config_path, + unsigned stale_days); + +/** + * Record successful password authentication for @p id. + * Creates or updates @c data/links/.ini and @c [link.] in hybbx.ini. + */ +hybbx_result_t hybbx_link_registry_touch(hybbx_link_registry_t *reg, + const char *id, + const char *role, + char *link_code_out, + size_t link_code_cap); + +/** + * Remove link entries with @c last_seen older than @p stale_days. + * Also drops matching @c [link.*] sections from hybbx.ini when @p config_path set. + */ +hybbx_result_t hybbx_link_registry_prune(hybbx_link_registry_t *reg); + +/** Generate a short link code (uppercase alphanumeric). */ +void hybbx_link_generate_code(char *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_LINK_H */ diff --git a/include/hybbx/log.h b/include/hybbx/log.h new file mode 100644 index 0000000..aa70b5b --- /dev/null +++ b/include/hybbx/log.h @@ -0,0 +1,64 @@ +#ifndef HYBBX_LOG_H +#define HYBBX_LOG_H + +#include "hybbx/types.h" +#include "hybbx/limits.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * File + console log severity (least to most severe for filtering): + * debug → stats → info → warn (default threshold). + * Messages at or above the configured [log] level are emitted. + */ +typedef enum hybbx_log_level { + HYBBX_LOG_DEBUG = 0, + HYBBX_LOG_STATS = 1, + HYBBX_LOG_INFO = 2, + HYBBX_LOG_WARN = 3 +} hybbx_log_level_t; + +typedef struct hybbx_log_config { + int enabled; + char dir[HYBBX_PATH_MAX]; + hybbx_log_level_t level; +} hybbx_log_config_t; + +void hybbx_log_config_defaults(hybbx_log_config_t *cfg); +void hybbx_log_config_apply(const struct hybbx_config *config); +const hybbx_log_config_t *hybbx_log_config_get(void); + +/** Non-zero when file logging is active. */ +int hybbx_log_enabled(void); + +/** + * Copy absolute path of the current hybbx log file into @p out. + * Returns HYBBX_OK when logging is enabled and a path is available. + */ +hybbx_result_t hybbx_log_current_path(char *out, size_t out_len); + +const char *hybbx_log_level_name(hybbx_log_level_t level); +hybbx_log_level_t hybbx_log_parse_level(const char *value); + +/** Non-zero when @p level would be written (console and file). */ +int hybbx_log_level_visible(hybbx_log_level_t level); + +void hybbx_log_write(hybbx_log_level_t level, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +void hybbx_log_shutdown(void); + +#define hybbx_log_debug(...) hybbx_log_write(HYBBX_LOG_DEBUG, __VA_ARGS__) +#define hybbx_log_stats(...) hybbx_log_write(HYBBX_LOG_STATS, __VA_ARGS__) +#define hybbx_log_info(...) hybbx_log_write(HYBBX_LOG_INFO, __VA_ARGS__) +#define hybbx_log_warn(...) hybbx_log_write(HYBBX_LOG_WARN, __VA_ARGS__) + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_LOG_H */ diff --git a/include/hybbx/mail.h b/include/hybbx/mail.h new file mode 100644 index 0000000..f96e1b6 --- /dev/null +++ b/include/hybbx/mail.h @@ -0,0 +1,129 @@ +#ifndef HYBBX_MAIL_H +#define HYBBX_MAIL_H + +#include "hybbx/config.h" +#include "hybbx/storage.h" +#include "hybbx/types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; +struct hybbx_session; + +/** Maximum stored messages per user inbox. */ +#define HYBBX_MAIL_MAX_MESSAGES 50u + +/** Subject line limit (80-column profile). */ +#define HYBBX_MAIL_SUBJECT_MAX 72u + +/** Total message body size (all lines). */ +#define HYBBX_MAIL_BODY_MAX 2048u + +/** Default days before recycled mail is permanently removed. */ +#define HYBBX_MAIL_DEFAULT_RECYCLE_DAYS 10u + +typedef struct hybbx_mail_config { + int enabled; + unsigned max_messages; + unsigned subject_max; + unsigned body_max; + unsigned recycle_days; + /** Root mail directory (typically /mail). */ + char root[512]; +} hybbx_mail_config_t; + +typedef struct hybbx_mail_entry { + uint64_t id; + char from[64]; + char subject[HYBBX_MAIL_SUBJECT_MAX]; + time_t received_at; + int read; +} hybbx_mail_entry_t; + +void hybbx_mail_config_defaults(hybbx_mail_config_t *mail); + +/** Load `[mail]` and set @p mail->root under @p storage_path. */ +void hybbx_mail_config_apply(hybbx_mail_config_t *mail, + const hybbx_config_t *config, + const char *storage_path); + +const hybbx_mail_config_t *hybbx_service_get_mail(const struct hybbx_service *service); + +/** List inbox on @p session (registered users only). */ +void hybbx_mail_list_inbox(struct hybbx_service *service, + struct hybbx_session *session); + +/** + * List inbox rows @p from..@p to (1-based, newest first). + * @p to = 0 means through the last message. + */ +void hybbx_mail_list_inbox_range(struct hybbx_service *service, + struct hybbx_session *session, + unsigned from, unsigned to); + +/** + * After login: if mail arrived since @p since_login, announce the count. + * No output when mail is disabled, the user is a guest, or the count is zero. + * @p since_login 0 means first login (all inbox messages count as new). + */ +void hybbx_mail_announce_since_last_login(struct hybbx_service *service, + struct hybbx_session *session, + time_t since_login); + +/** + * Parse @p spec as @c from-to (e.g. @c 1-15 , @c 5-20 ) or a single index. + * Returns 0 on invalid input. + */ +int hybbx_mail_parse_list_range(const char *spec, + unsigned *from, unsigned *to); + +/** + * Read message by 1-based list index (newest first). + * Marks the message as read. + */ +hybbx_result_t hybbx_mail_read(struct hybbx_service *service, + struct hybbx_session *session, + unsigned list_index); + +/** Move inbox message(s) to recycle (1-based index or range). */ +hybbx_result_t hybbx_mail_delete(struct hybbx_service *service, + struct hybbx_session *session, + unsigned list_index); + +hybbx_result_t hybbx_mail_delete_range(struct hybbx_service *service, + struct hybbx_session *session, + unsigned from, unsigned to); + +/** Permanently remove all messages in the user's recycle bin. */ +hybbx_result_t hybbx_mail_recycle_empty(struct hybbx_service *service, + struct hybbx_session *session); + +/** + * Deliver a message from @p from_user to @p to_user. + * @p to_user must exist and be an active registered account. + */ +hybbx_result_t hybbx_mail_deliver(struct hybbx_service *service, + const char *from_user, + const char *to_user, + const char *subject, + const char *body); + +/** + * Mail active Sysop and Admin when a guest self-registers. + * Body lists every field from `/register` plus assigned account metadata. + * No-op when mail is disabled. Individual delivery failures are ignored. + */ +hybbx_result_t hybbx_mail_notify_staff_registration( + struct hybbx_service *service, + const hybbx_user_registration_t *reg, + const hybbx_user_record_t *user); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_MAIL_H */ diff --git a/include/hybbx/mains_proxy.h b/include/hybbx/mains_proxy.h new file mode 100644 index 0000000..7d91c87 --- /dev/null +++ b/include/hybbx/mains_proxy.h @@ -0,0 +1,120 @@ +#ifndef HYBBX_MAINS_PROXY_H +#define HYBBX_MAINS_PROXY_H + +/** + * Main-to-Main mesh proxy — link HyBBX instances for user services only. + * + * SECURITY: inter-node mesh links MUST attach via HBX/Circuit only + * (hybbx_circuit_link_connect + LINK_AUTH). Never open a raw TCP socket + * to a remote Main or peer process — same rule as Secondary edge links. + * + * Only proxymail and proxychat service payloads cross the mesh. No user + * accounts, rights, or other Main data are transferred. + */ + +#include "hybbx/circuit.h" +#include "hybbx/config.h" +#include "hybbx/link.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_MAINS_PROXY_PEER_ID_MAX 64 +#define HYBBX_MAINS_PROXY_HOST_MAX 256 +#define HYBBX_MAINS_PROXY_MAX_PEERS 8u + +typedef enum hybbx_mains_proxy_wire { + /** IP path carried on HBX circuit frames (not a direct Main socket). */ + HYBBX_MAINS_PROXY_WIRE_CIRCUIT = 0, + HYBBX_MAINS_PROXY_WIRE_AX25, +} hybbx_mains_proxy_wire_t; + +typedef enum hybbx_mains_proxy_duplex { + HYBBX_MAINS_PROXY_DUPLEX_FULL = 0, + HYBBX_MAINS_PROXY_DUPLEX_HALF, +} hybbx_mains_proxy_duplex_t; + +typedef struct hybbx_mains_proxy_peer_config { + char peer_id[HYBBX_MAINS_PROXY_PEER_ID_MAX]; + char circuit_host[HYBBX_MAINS_PROXY_HOST_MAX]; + unsigned circuit_port; + char link_id[HYBBX_LINK_ID_MAX]; + char link_password[128]; + /** @deprecated Ignored for mesh connect — use circuit_host. */ + char host[HYBBX_MAINS_PROXY_HOST_MAX]; + /** @deprecated Ignored for mesh connect — use circuit_port. */ + unsigned port; + hybbx_mains_proxy_wire_t wire; + hybbx_mains_proxy_duplex_t duplex; + int use_secondary; + int enabled; +} hybbx_mains_proxy_peer_config_t; + +typedef struct hybbx_mains_proxy_mesh { + unsigned peer_count; + hybbx_mains_proxy_peer_config_t peers[HYBBX_MAINS_PROXY_MAX_PEERS]; + int running; +} hybbx_mains_proxy_mesh_t; + +struct hybbx_service; + +hybbx_mains_proxy_wire_t hybbx_mains_proxy_wire_parse(const char *value); +const char *hybbx_mains_proxy_wire_name(hybbx_mains_proxy_wire_t wire); + +hybbx_mains_proxy_duplex_t hybbx_mains_proxy_duplex_parse(const char *value); +const char *hybbx_mains_proxy_duplex_name(hybbx_mains_proxy_duplex_t duplex); + +void hybbx_mains_proxy_peer_defaults(hybbx_mains_proxy_peer_config_t *peer); + +/** Parse one `transport.mains_proxy` / `transport.mains_proxyN` section. */ +hybbx_result_t hybbx_mains_proxy_peer_parse(const char *config, + hybbx_mains_proxy_peer_config_t *out); + +void hybbx_mains_proxy_mesh_init(hybbx_mains_proxy_mesh_t *mesh); + +/** + * Register peers and start mesh. Existing mesh state is replaced when called + * again while running. + */ +hybbx_result_t hybbx_mains_proxy_mesh_start(struct hybbx_service *service, + hybbx_mains_proxy_mesh_t *mesh); + +void hybbx_mains_proxy_mesh_stop(hybbx_mains_proxy_mesh_t *mesh); + +/** Periodic mesh I/O. Call from service main loop when running. */ +void hybbx_mains_proxy_mesh_tick(struct hybbx_service *service, + hybbx_mains_proxy_mesh_t *mesh); + +/** Non-zero when the mesh relay is active with at least one live peer link. */ +int hybbx_mains_proxy_mesh_active(void); + +/** + * Deliver proxymail to a configured peer (HBX PROXY_MAIL frame). + * @p from_address and @p to_address use @c user@service form. + */ +hybbx_result_t hybbx_mains_proxy_send_mail(struct hybbx_service *service, + const char *from_address, + const char *to_address, + const char *subject, + const char *body); + +/** Relay a proxychat line to all connected peers (HBX PROXY_CHAT frame). */ +hybbx_result_t hybbx_mains_proxy_send_chat(struct hybbx_service *service, + const char *from_address, + const char *line); + +/** Handle PROXY_MAIL / PROXY_CHAT received on the circuit hub (inbound link). */ +void hybbx_mains_proxy_inbound_frame(struct hybbx_service *service, + hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len); + +/** Service-loop tick (implemented by plugin when built). */ +void hybbx_mains_proxy_plugin_tick(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_MAINS_PROXY_H */ diff --git a/include/hybbx/max25.h b/include/hybbx/max25.h new file mode 100644 index 0000000..5bae5c3 --- /dev/null +++ b/include/hybbx/max25.h @@ -0,0 +1,72 @@ +#ifndef HYBBX_MAX25_H +#define HYBBX_MAX25_H + +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default max25d TCP listen address. */ +#define HYBBX_MAX25_DEFAULT_HOST "127.0.0.1" + +typedef struct hybbx_max25_config { + int check; + char host[HYBBX_CONFIG_VALUE_MAX]; + unsigned port; + unsigned timeout_ms; +} hybbx_max25_config_t; + +/** Snapshot from max25d M25/1 STATUS (read-only; HyBBX never overrides). */ +typedef struct hybbx_max25_status { + int handshake_ok; + char error[16]; + char voice[16]; + char stack[32]; + char serial[64]; +} hybbx_max25_status_t; + +void hybbx_max25_config_defaults(hybbx_max25_config_t *cfg); + +/** Load `[max25]` from INI. `check` defaults to `yes`. Reporting is MAX25-only. */ +void hybbx_max25_config_apply(hybbx_max25_config_t *cfg, + const hybbx_config_t *config); + +/** Parse a semicolon key=value chunk (`max25_check=…` keys). */ +void hybbx_max25_config_parse_kv(const char *max25_kv, + hybbx_max25_config_t *cfg); + +/** + * When @p config starts with a max25 prefix (before + * @ref HYBBX_PACKET_RADIO_INSTANCE_SEP), parse it and return the remainder. + * When no prefix is present, @p cfg->check is cleared. + */ +const char *hybbx_max25_config_skip_prefix(const char *config, + hybbx_max25_config_t *cfg); + +void hybbx_max25_status_clear(hybbx_max25_status_t *status); + +/** + * TCP connect + M25/1 connect handshake (`OK` + `STATUS …`). + * Returns @ref HYBBX_OK when max25d responds; @p status_out receives + * `error=` / `voice=` / `stack=` / `serial=` as reported by MAX25 (valid or invalid). + * Data-quality rules (3×20s passes, CALLID, min % good) live in max25d only — + * HyBBX accepts STATUS without override; only TCP/handshake failure is fatal. + */ +hybbx_result_t hybbx_max25_probe(const hybbx_max25_config_t *cfg, + hybbx_max25_status_t *status_out); + +/** + * When @p cfg->check is set, block until max25d responds or + * @ref HYBBX_MAX25_PROBE_WAIT_MS elapses. + * Returns @ref HYBBX_OK when max25d is reachable; @ref HYBBX_ERR_IO on timeout. + */ +hybbx_result_t hybbx_max25_wait_ready(const hybbx_max25_config_t *cfg); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_MAX25_H */ diff --git a/include/hybbx/messages.h b/include/hybbx/messages.h new file mode 100644 index 0000000..d1c7345 --- /dev/null +++ b/include/hybbx/messages.h @@ -0,0 +1,58 @@ +#ifndef HYBBX_MESSAGES_H +#define HYBBX_MESSAGES_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_session; + +/** Automatic system notices (login, mail, timeouts, conference status, …). */ +#define HYBBX_MSG_PREFIX_SYSTEM "*** " + +/** Sysop / granted manual system-wide notices (`/broadcast`, `/announce`). */ +#define HYBBX_MSG_PREFIX_SYSOP "+++ " + +/** Private directed messages (system or Sysop+granted); v1 half-stub. */ +#define HYBBX_MSG_PREFIX_PRIVATE "### " + +/** + * Format a system line: `*** ` + */ +hybbx_result_t hybbx_msg_format_system(char *out, size_t out_len, + const char *body); + +/** + * Format a Sysop/granted system-wide line: `+++ : ` + * @p from_label may be NULL → `+++ ` + */ +hybbx_result_t hybbx_msg_format_sysop(char *out, size_t out_len, + const char *from_label, + const char *body); + +/** + * Format a private line: `### : ` or `### ` when @p from_label is NULL. + */ +hybbx_result_t hybbx_msg_format_private(char *out, size_t out_len, + const char *from_label, + const char *body); + +hybbx_result_t hybbx_msg_send_system(struct hybbx_session *session, + const char *body); + +hybbx_result_t hybbx_msg_send_sysop(struct hybbx_session *session, + const char *from_label, + const char *body); + +/** Half-stub: directed private message to one session. */ +hybbx_result_t hybbx_msg_send_private(struct hybbx_session *session, + const char *from_label, + const char *body); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_MESSAGES_H */ diff --git a/include/hybbx/monitor.h b/include/hybbx/monitor.h new file mode 100644 index 0000000..a4d8db5 --- /dev/null +++ b/include/hybbx/monitor.h @@ -0,0 +1,74 @@ +#ifndef HYBBX_MONITOR_H +#define HYBBX_MONITOR_H + +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_config; +struct hybbx_service; +struct hybbx_session; + +#define HYBBX_MONITOR_ALLOW_MAX 16u +#define HYBBX_MONITOR_ALLOW_NAME_MAX 64u + +typedef struct hybbx_monitor_config { + int enabled; + int follow_hybbx; + int follow_security; + /** When set, Sysop sessions are always hidden from /who and /online. */ + int invisible_sysop; + unsigned invite_timeout_sec; + char allow[HYBBX_MONITOR_ALLOW_MAX][HYBBX_MONITOR_ALLOW_NAME_MAX]; + unsigned allow_count; +} hybbx_monitor_config_t; + +void hybbx_monitor_config_defaults(hybbx_monitor_config_t *cfg); +void hybbx_monitor_config_apply(const struct hybbx_config *config); +const hybbx_monitor_config_t *hybbx_monitor_config_get(void); + +/** Non-zero when [monitor] enabled=yes. */ +int hybbx_monitor_enabled(void); + +/** Non-zero when [monitor] invisible-sysop=yes (Sysop always /who-hidden). */ +int hybbx_monitor_invisible_sysop(void); + +/** Non-zero when @p username is listed in [monitor] allow=. */ +int hybbx_monitor_user_allowed(const char *username); + +/** + * Non-zero when @p session may use /monitor (Sysop/registry OR allow list). + * Caller still runs normal command access for registry min-level. + */ +int hybbx_monitor_session_may_use(const struct hybbx_session *session); + +/** Enable/disable monitor mode on @p session. */ +hybbx_result_t hybbx_monitor_set_active(struct hybbx_session *session, int on); + +int hybbx_monitor_is_active(const struct hybbx_session *session); + +/** + * Push a live line to every session with monitor mode on. + * Prefixes with "[monitor] " for clarity. + */ +void hybbx_monitor_broadcast(struct hybbx_service *service, const char *line); + +/** Format Username@plugin event and push live (always when any monitor on). */ +void hybbx_monitor_event(struct hybbx_service *service, + const char *username, + const char *plugin, + const char *detail); + +/** Poll log file tails (call from service loop). */ +void hybbx_monitor_tick(struct hybbx_service *service); + +void hybbx_monitor_shutdown(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_MONITOR_H */ diff --git a/include/hybbx/networks.h b/include/hybbx/networks.h new file mode 100644 index 0000000..ab8e954 --- /dev/null +++ b/include/hybbx/networks.h @@ -0,0 +1,63 @@ +#ifndef HYBBX_NETWORKS_H +#define HYBBX_NETWORKS_H + +#include "hybbx/config.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Connection / link adapter switches from INI `[networks]`. + * + * Static transport depends on instance role (see hybbx/instance.h): + * Main — websocket (always on when built) + * Secondary — mains_proxy + * Proxy — none (optional TCP transports) + */ +typedef struct hybbx_networks_config { + /** Telnet (`transport.telnet`) — not used on Main (WebSocket only). */ + int telnet; + /** AX.25 / packet radio — always forced off (MAX25-Stack owns RF). */ + int ax25; + /** BayCom — always forced off (MAX25-Stack owns RF). */ + int baycom; + /** ARDOP — always forced off (MAX25-Stack owns RF / soft-modem). */ + int ardop; + /** CRDOP — always forced off (MAX25-Stack owns RF / soft-modem). */ + int crdop; + /** SSH transport (`transport.ssh`). */ + int ssh; + /** WebSocket forward-proxy (`transport.websocket`) — Main static path. */ + int websocket; + /** Internal HBX circuit hub / attach. */ + int circuit; + /** Main-to-Main mesh (`transport.mains_proxy`) — Secondary primary. */ + int mains_proxy; +} hybbx_networks_config_t; + +void hybbx_networks_config_defaults(hybbx_networks_config_t *networks); + +/** Load `[networks]` from an INI file. */ +void hybbx_networks_config_apply(hybbx_networks_config_t *networks, + const hybbx_config_t *config); + +/** + * Non-zero when @p plugin_name is the role's static core transport. + * Ignores `[transport.*] enabled` and is started when built. + */ +int hybbx_networks_is_static_transport(const char *plugin_name); + +/** + * Non-zero when transport @p plugin_name should be started for this config. + * Respects instance plugin allow-list and `[networks]` toggles. + */ +int hybbx_networks_transport_wanted(const char *plugin_name, + const hybbx_networks_config_t *networks); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_NETWORKS_H */ diff --git a/include/hybbx/packet_radio.h b/include/hybbx/packet_radio.h new file mode 100644 index 0000000..51f79a0 --- /dev/null +++ b/include/hybbx/packet_radio.h @@ -0,0 +1,57 @@ +#ifndef HYBBX_PACKET_RADIO_H +#define HYBBX_PACKET_RADIO_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default line speed for packet radio (2400 baud). */ +#define HYBBX_PACKET_RADIO_DEFAULT_BAUD 2400 + +/** Default USB AX.25 TNC/modem device (Linux). */ +#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_USB "/dev/ttyUSB0" + +#if defined(_WIN32) +#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL "COM1" +#elif defined(__AMIGA__) +#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL "serial.device" +#else +#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL "/dev/ttyS0" +#endif + +typedef enum hybbx_packet_radio_device_type { + HYBBX_PACKET_RADIO_DEVICE_USB = 1, + HYBBX_PACKET_RADIO_DEVICE_SERIAL = 2 +} hybbx_packet_radio_device_type_t; + +struct hybbx_packet_radio_config; + +/** + * Parse a semicolon-separated key=value transport config string. + * Missing keys receive HyBBX defaults (TNC2C, USB, KISS @ 2400 baud). + * Full config type: include hybbx/tnc.h. + */ +hybbx_result_t hybbx_packet_radio_config_parse(const char *config, + struct hybbx_packet_radio_config *out); + +void hybbx_packet_radio_config_free(struct hybbx_packet_radio_config *config); + +/** + * Non-zero when @p config starts a local TNC (device/tnc/protocol/circuit_host). + * Main bridge-registry rows (link_id only) return zero. + */ +int hybbx_packet_radio_section_is_local_edge(const char *config); + +/** + * Return non-zero when @p baud is valid for packet radio configuration. + * HyBBX accepts any positive host baud rate; TNC2C documents 300..38400. + */ +int hybbx_packet_radio_baud_valid(unsigned int baud); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_PACKET_RADIO_H */ diff --git a/include/hybbx/password.h b/include/hybbx/password.h new file mode 100644 index 0000000..cc1b38e --- /dev/null +++ b/include/hybbx/password.h @@ -0,0 +1,53 @@ +#ifndef HYBBX_PASSWORD_H +#define HYBBX_PASSWORD_H + +#include "hybbx/types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Stored password prefix for SHA-256 (default, tinysha256). */ +#define HYBBX_PASSWORD_SHA256_PREFIX "{sha256}" +/** Stored password prefix for legacy MD5 (verify-only fallback). */ +#define HYBBX_PASSWORD_MD5_PREFIX "{md5}" + +/** Max stored password field: prefix + 64 hex digits + NUL. */ +#define HYBBX_PASSWORD_STORED_MAX 96 + +/** Non-zero when @p stored is a hashed password (sha256 or md5 prefix). */ +int hybbx_password_is_hashed(const char *stored); + +/** Non-zero when @p stored is plain text and should be upgraded on load. */ +int hybbx_password_is_plain(const char *stored); + +/** + * Hash @p plain into @p out as {sha256} using the configured backend + * (default: bundled tinysha256; see [crypto] password_hash in hybbx.ini). + */ +hybbx_result_t hybbx_password_hash_sha256(const char *plain, + char *out, + size_t out_size); + +/** Hash @p plain with the configured default algorithm (SHA-256). */ +hybbx_result_t hybbx_password_hash(const char *plain, char *out, size_t out_size); + +/** Return non-zero when @p provided matches @p stored (plain, sha256, or md5). */ +int hybbx_password_match(const char *stored, const char *provided); + +/** + * Fill @p out with a random password of length @p min_len … @p max_len using + * characters a-z and 0-9 only. + */ +hybbx_result_t hybbx_password_generate_alnum(char *out, + size_t out_size, + size_t min_len, + size_t max_len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_PASSWORD_H */ diff --git a/include/hybbx/plugin.h b/include/hybbx/plugin.h new file mode 100644 index 0000000..ea72a65 --- /dev/null +++ b/include/hybbx/plugin.h @@ -0,0 +1,75 @@ +#ifndef HYBBX_PLUGIN_H +#define HYBBX_PLUGIN_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; +struct hybbx_session; + +/** + * Transport plugin interface (link adapters / host-client bridges). + * + * HyBBX is plugin-only: session core + plugins. Modems, TNCs, sound-card + * software, ARDOPC, and CRDOPC are external — never embedded in HyBBX. + * + * Core uses TCP/IPv4+IPv6 and HBX internally. Plugins terminate the wire + * to external services (telnet TCP, serial TNC, ARDOP host TCP, …) and + * expose a byte stream to hybbx_session via the write callback. + */ +typedef struct hybbx_transport_plugin { + const char *name; + hybbx_transport_kind_t kind; + unsigned int version; + + /** Called once when the plugin is loaded. */ + hybbx_result_t (*init)(struct hybbx_service *service); + + /** Called once when the plugin is unloaded. */ + void (*shutdown)(void); + + /** + * Start listening / accepting connections on the given bind address. + * @p config is a semicolon-separated key=value string built from the + * transport's INI section (e.g. "bind=0.0.0.0;port=2323"). + */ + hybbx_result_t (*start)(const char *config); + + /** Stop accepting new connections; existing sessions may continue. */ + hybbx_result_t (*stop)(void); + + /** + * Send raw bytes to the connected client for this session. + * NULL → core writes to stdout (development fallback). + */ + hybbx_result_t (*write)(struct hybbx_session *session, + const char *data, size_t len); + +} hybbx_transport_plugin_t; + +/** + * Session callbacks invoked by a transport plugin when a client connects. + */ +typedef struct hybbx_session_ops { + hybbx_result_t (*on_connect)(struct hybbx_session *session, void *userdata); + hybbx_result_t (*on_data)(struct hybbx_session *session, + const uint8_t *data, size_t len, + void *userdata); + void (*on_disconnect)(struct hybbx_session *session, void *userdata); +} hybbx_session_ops_t; + +/** Opaque session handle passed between core and transport plugins. */ +typedef struct hybbx_session { + const hybbx_transport_plugin_t *transport; + void *transport_data; + void *core_data; +} hybbx_session_t; + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_PLUGIN_H */ diff --git a/include/hybbx/posix_time.h b/include/hybbx/posix_time.h new file mode 100644 index 0000000..6790120 --- /dev/null +++ b/include/hybbx/posix_time.h @@ -0,0 +1,21 @@ +#ifndef HYBBX_POSIX_TIME_H +#define HYBBX_POSIX_TIME_H + +/* + * clock_gettime(CLOCK_MONOTONIC) and usleep() on glibc/Linux (GCC 15+ C99). + * Include before / in .c files, or rely on CMake + * _DEFAULT_SOURCE on Linux builds. + */ +#if defined(__linux__) || defined(__GLIBC__) +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#endif + +#include + +#if !defined(_WIN32) +#include +#endif + +#endif /* HYBBX_POSIX_TIME_H */ diff --git a/include/hybbx/privilege.h b/include/hybbx/privilege.h new file mode 100644 index 0000000..df646c8 --- /dev/null +++ b/include/hybbx/privilege.h @@ -0,0 +1,24 @@ +#ifndef HYBBX_PRIVILEGE_H +#define HYBBX_PRIVILEGE_H + +#include "hybbx/config.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Apply [service] user=/group=/uid=/gid= from @p config. + * + * Linux: hybbxd must not keep running as root. When euid==0, @c user is + * required and privileges drop immediately via setuid/setgid. + * Non-root starts are unchanged (optional @c user must match effective uid). + */ +hybbx_result_t hybbx_privilege_apply_from_config(const hybbx_config_t *config); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_PRIVILEGE_H */ diff --git a/include/hybbx/proxychat.h b/include/hybbx/proxychat.h new file mode 100644 index 0000000..74f71f2 --- /dev/null +++ b/include/hybbx/proxychat.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_PROXYCHAT_H +#define HYBBX_PROXYCHAT_H + +/** + * Inter-Main chat via mains_proxy. + * + * Local `/chat` stays on this Main. `/proxychat` is the mains-proxy sub-area + * under `/chat` (or `/chat proxychat`). Only chat lines cross the mesh — no + * user accounts or rights. + */ + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; +struct hybbx_session; + +/** Show proxychat area banner. */ +void hybbx_proxychat_show_banner(struct hybbx_session *session); + +/** + * Post a line in proxychat (relay when mesh is active). + */ +hybbx_result_t hybbx_proxychat_post(struct hybbx_service *service, + struct hybbx_session *session, + const char *line); + +/** Deliver a line received from a remote Main. */ +void hybbx_proxychat_receive(struct hybbx_service *service, + const char *from_address, + const char *line); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_PROXYCHAT_H */ diff --git a/include/hybbx/proxymail.h b/include/hybbx/proxymail.h new file mode 100644 index 0000000..123b663 --- /dev/null +++ b/include/hybbx/proxymail.h @@ -0,0 +1,73 @@ +#ifndef HYBBX_PROXYMAIL_H +#define HYBBX_PROXYMAIL_H + +/** + * Inter-Main mail via mains_proxy — separate from local `/mail`. + * + * Local mail uses flat-file or SQL under `[mail]`; proxymail uses flat files + * under `data/proxymail/` on each node. Only service payloads cross the mesh. + */ + +#include "hybbx/mail.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** `username@remote-main-service` address string. */ +#define HYBBX_PROXYMAIL_ADDRESS_MAX 128u +#define HYBBX_PROXYMAIL_SERVICE_NAME_MAX 64u + +struct hybbx_service; +struct hybbx_session; + +/** + * Parse @p address as @c user@remote-service . + * @return 0 when invalid. + */ +int hybbx_proxymail_parse_address(const char *address, + char *user, size_t user_len, + char *remote_service, size_t remote_len); + +/** List proxymail inbox (stub). */ +void hybbx_proxymail_list_inbox(struct hybbx_service *service, + struct hybbx_session *session); + +void hybbx_proxymail_list_inbox_range(struct hybbx_service *service, + struct hybbx_session *session, + unsigned from, unsigned to); + +hybbx_result_t hybbx_proxymail_read(struct hybbx_service *service, + struct hybbx_session *session, + unsigned list_index); + +hybbx_result_t hybbx_proxymail_delete_range(struct hybbx_service *service, + struct hybbx_session *session, + unsigned from, unsigned to); + +hybbx_result_t hybbx_proxymail_recycle_empty(struct hybbx_service *service, + struct hybbx_session *session); + +/** + * Queue outbound proxymail for mesh delivery. + * @p to_address must be @c user@remote-main-service . + */ +hybbx_result_t hybbx_proxymail_deliver(struct hybbx_service *service, + const char *from_user, + const char *to_address, + const char *subject, + const char *body); + +/** Store proxymail received from a remote Main. */ +void hybbx_proxymail_receive(struct hybbx_service *service, + const char *from_address, + const char *to_address, + const char *subject, + const char *body); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_PROXYMAIL_H */ diff --git a/include/hybbx/registry.h b/include/hybbx/registry.h new file mode 100644 index 0000000..51577ee --- /dev/null +++ b/include/hybbx/registry.h @@ -0,0 +1,28 @@ +#ifndef HYBBX_REGISTRY_H +#define HYBBX_REGISTRY_H + +#include "hybbx/plugin.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Register a transport plugin (called at plugin init time). */ +hybbx_result_t hybbx_registry_register(const hybbx_transport_plugin_t *plugin); + +/** Unregister a transport plugin by name. */ +hybbx_result_t hybbx_registry_unregister(const char *name); + +/** Look up a registered plugin by name. Returns NULL if not found. */ +const hybbx_transport_plugin_t *hybbx_registry_find(const char *name); + +/** Iterate over all registered plugins. */ +typedef void (*hybbx_registry_iter_fn)(const hybbx_transport_plugin_t *plugin, + void *userdata); +void hybbx_registry_foreach(hybbx_registry_iter_fn fn, void *userdata); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_REGISTRY_H */ diff --git a/include/hybbx/rf_tx_pace.h b/include/hybbx/rf_tx_pace.h new file mode 100644 index 0000000..f987490 --- /dev/null +++ b/include/hybbx/rf_tx_pace.h @@ -0,0 +1,16 @@ +#ifndef HYBBX_RF_TX_PACE_H +#define HYBBX_RF_TX_PACE_H + +/** Minimum idle time between RF frame sends (HyBBX packet radio). */ +#define HYBBX_RF_TX_MIN_GAP_MS 1500u + +/** Maximum continuous RF send burst (wall clock); new burst after MIN_GAP idle. */ +#define HYBBX_RF_TX_MAX_BURST_MS 15000u + +/** Min gap + max burst (TNC / direct serial). */ +void hybbx_rf_tx_pace(void); + +/** Max burst only — use when max25-bcpr enforces min gap on KISS (baycom plugin). */ +void hybbx_rf_tx_burst_guard(void); + +#endif diff --git a/include/hybbx/security.h b/include/hybbx/security.h new file mode 100644 index 0000000..f4a9476 --- /dev/null +++ b/include/hybbx/security.h @@ -0,0 +1,31 @@ +#ifndef HYBBX_SECURITY_H +#define HYBBX_SECURITY_H + +#include "hybbx/types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_config; + +/** Log file name under the configured logs directory. */ +#define HYBBX_SECURITY_LOG_FILE "security.log" + +void hybbx_security_log_config_apply(const struct hybbx_config *config); +void hybbx_security_log_shutdown(void); + +/** Append one line to security.log (timestamp added). Always enabled when the log directory is available. */ +void hybbx_security_log_write(const char *fmt, ...) + __attribute__((format(printf, 1, 2))); + +/** Absolute path to security.log when the security log is ready. */ +hybbx_result_t hybbx_security_log_current_path(char *out, size_t out_len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SECURITY_H */ diff --git a/include/hybbx/security_ban.h b/include/hybbx/security_ban.h new file mode 100644 index 0000000..9e63757 --- /dev/null +++ b/include/hybbx/security_ban.h @@ -0,0 +1,80 @@ +#ifndef HYBBX_SECURITY_BAN_H +#define HYBBX_SECURITY_BAN_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_config; + +typedef enum hybbx_ban_backend { + HYBBX_BAN_BACKEND_INTERNAL = 0, + HYBBX_BAN_BACKEND_LOG, + HYBBX_BAN_BACKEND_IPTABLES, + HYBBX_BAN_BACKEND_NFTABLES, + HYBBX_BAN_BACKEND_HOSTS +} hybbx_ban_backend_t; + +void hybbx_security_ban_config_apply(const struct hybbx_config *config); +void hybbx_security_ban_shutdown(void); + +/** Expire bans and prune stale failure/rate records (service loop). */ +void hybbx_security_ban_tick(void); + +/** Non-zero when @p ip is currently banned. */ +int hybbx_security_ban_is_banned(const char *ip); + +/** + * Normalize @p in into @p out (uppercase CALL/CALL-SSID or link_id). + * Returns non-zero when valid. + */ +int hybbx_security_callid_normalize(const char *in, char *out, size_t out_cap); + +/** Non-zero when @p callid is banned (AX.25 callsign or HBX link_id). */ +int hybbx_security_ban_callid_is_banned(const char *callid); + +/** + * Accept gate for HBX link_id / AX.25 source before circuit or RF uplink. + * Returns non-zero when allowed. + */ +int hybbx_security_ban_callid_accept(const char *callid); + +/** + * Accept gate: rate-limit check + ban check. + * Returns non-zero when the connection may proceed. + */ +int hybbx_security_ban_accept(const char *ip); + +/** + * Same as @ref hybbx_security_ban_accept using the peer address of @p fd. + * Allows the connection when the peer address cannot be resolved. + */ +int hybbx_security_ban_accept_fd(int fd); + +/** Record a failed login for @p transport (telnet, ssh, websocket, …). */ +void hybbx_security_ban_login_fail(const char *ip, const char *transport); + +/** Record a failed HBX circuit link authentication (IP). */ +void hybbx_security_ban_link_auth_fail(const char *ip); + +/** Record a failed HBX circuit link authentication (link_id / CALLID). */ +void hybbx_security_ban_link_auth_fail_callid(const char *callid); + +/** + * Record excessive abuse (chat/mail flood, register spam, …). + * Uses @c abuse_maxretry / @c abuse_findtime — not login @c maxretry. + * Normal traffic limits ([chat], [mail], [traffic]) never call this. + */ +void hybbx_security_ban_abuse_report(const char *ip, const char *category); + +/** Same as @ref hybbx_security_ban_abuse_report for @p callid. */ +void hybbx_security_ban_callid_abuse_report(const char *callid, + const char *category); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SECURITY_BAN_H */ diff --git a/include/hybbx/service.h b/include/hybbx/service.h new file mode 100644 index 0000000..4730b8b --- /dev/null +++ b/include/hybbx/service.h @@ -0,0 +1,174 @@ +#ifndef HYBBX_SERVICE_H +#define HYBBX_SERVICE_H + +/** + * Centralized HyBBX daemon API (`hybbx` binary). + * + * Loads INI ([service], [storage], [auth], [transport.*], [circuit]), starts + * transport plugins and the HBX circuit hub. Edge link/repeater daemons attach + * to [circuit]; see share/hybbx.ini.example and docs/TOPOLOGY.md. + */ + +#include "hybbx/config.h" +#include "hybbx/plugin.h" +#include "hybbx/auth.h" +#include "hybbx/storage.h" +#include "hybbx/texts.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default global service name (`[service] name` in INI). */ +#define HYBBX_DEFAULT_SERVICE_NAME "hyBBX" + +typedef struct hybbx_service { + const char *name; + void *userdata; +} hybbx_service_t; + +/** Create the main HyBBX service instance. */ +hybbx_service_t *hybbx_service_create(const char *name); + +/** Destroy the service and shut down all active transports. */ +void hybbx_service_destroy(hybbx_service_t *service); + +/** + * Apply settings from an INI configuration (service name, enabled transports). + */ +hybbx_result_t hybbx_service_apply_config(hybbx_service_t *service, + const hybbx_config_t *config, + const char *config_path); + +/** + * Load a transport plugin by name (e.g. "telnet", "packet_radio"). + * Built-in plugins are linked statically; dynamic loading comes later. + */ +hybbx_result_t hybbx_service_load_transport(hybbx_service_t *service, + const char *plugin_name); + +/** Start a loaded transport with transport-specific configuration. */ +hybbx_result_t hybbx_service_start_transport(hybbx_service_t *service, + const char *plugin_name, + const char *config); + +/** Stop a running transport. */ +hybbx_result_t hybbx_service_stop_transport(hybbx_service_t *service, + const char *plugin_name); + +/** Run the main event loop until hybbx_service_stop() is called. */ +hybbx_result_t hybbx_service_run(hybbx_service_t *service); + +/** Request the main loop to exit. */ +void hybbx_service_stop(hybbx_service_t *service); + +typedef enum hybbx_shutdown_mode { + HYBBX_SHUTDOWN_NONE = 0, + HYBBX_SHUTDOWN_STOP = 1, + HYBBX_SHUTDOWN_RESTART = 2 +} hybbx_shutdown_mode_t; + +/** Store the daemon binary path used for /restart (typically argv[0]). */ +void hybbx_service_set_launch_binary(hybbx_service_t *service, const char *argv0); + +/** Sysop /shutdown or /restart — stops the main loop; restart re-execs after exit. */ +void hybbx_service_request_shutdown(hybbx_service_t *service, int restart); + +/** Shutdown mode requested while the service is still running. */ +hybbx_shutdown_mode_t hybbx_service_shutdown_mode(const hybbx_service_t *service); + +/** + * Re-exec the daemon when @ref hybbx_service_shutdown_mode is + * @ref HYBBX_SHUTDOWN_RESTART. Call before @ref hybbx_service_destroy. + * Does not return on success. + */ +void hybbx_service_restart_exec(const hybbx_service_t *service); + +const char *hybbx_service_config_path(const hybbx_service_t *service); + +hybbx_storage_t *hybbx_service_get_storage(hybbx_service_t *service); +const hybbx_auth_config_t *hybbx_service_get_auth(hybbx_service_t *service); +const hybbx_texts_config_t *hybbx_service_get_texts(hybbx_service_t *service); + +/** + * Global input prompt shown to every connected user. + * Empty string when unset (default): no visible prompt before input. + */ +const char *hybbx_service_get_prompt(const hybbx_service_t *service); + +/** Global service name from configuration (default @ref HYBBX_DEFAULT_SERVICE_NAME). */ +const char *hybbx_service_get_name(const hybbx_service_t *service); + +/** Configured maximum simultaneous online sessions (default @ref HYBBX_DEFAULT_MAX_ONLINE). */ +unsigned hybbx_service_max_online(const hybbx_service_t *service); + +/** + * Non-zero when `[service] login_announce=yes`: + * broadcast "*** User login: user@plugin" and list /who as user@plugin. + */ +int hybbx_service_login_announce(const hybbx_service_t *service); + +/** Currently connected sessions (all transports). */ +unsigned hybbx_service_active_nodes(const hybbx_service_t *service); + +/** Guest auto-disconnect timeout in seconds (from INI minutes setting). */ +unsigned hybbx_service_guest_timeout_seconds(const hybbx_service_t *service); + +struct hybbx_user_record; + +/** + * Assign the lowest free ephemeral guest slot (Guest1 … Guest25). + * Guests are not written to user files. + */ +hybbx_result_t hybbx_service_guest_assign(hybbx_service_t *service, + const char *guest_prefix, + struct hybbx_user_record *out, + unsigned *slot_out); + +/** Release a guest slot when the session ends or leaves guest mode. */ +void hybbx_service_guest_release(hybbx_service_t *service, unsigned slot); + +/** + * Reserve one node slot for a new connection. + * @return HYBBX_ERR_BUSY when @ref hybbx_service_max_online is reached. + */ +hybbx_result_t hybbx_service_acquire_node(hybbx_service_t *service); + +/** Release a node slot (pair with @ref hybbx_service_acquire_node). */ +void hybbx_service_release_node(hybbx_service_t *service); + +struct hybbx_session; + +/** Track an active connection for chat broadcast and similar features. */ +hybbx_result_t hybbx_service_attach_session(hybbx_service_t *service, + struct hybbx_session *session); + +/** Remove a session from the active connection list. */ +void hybbx_service_detach_session(hybbx_service_t *service, + struct hybbx_session *session); + +typedef void (*hybbx_service_session_visit_fn)(struct hybbx_session *session, + void *userdata); + +/** Invoke @p fn for each attached session (snapshot; lock not held in @p fn). */ +void hybbx_service_visit_sessions(hybbx_service_t *service, + hybbx_service_session_visit_fn fn, + void *userdata); + +/** + * Return a logged-in registered session for @p user_id on another connection, + * or NULL when the account is not online elsewhere. + */ +struct hybbx_session *hybbx_service_find_registered_session( + hybbx_service_t *service, + uint64_t user_id, + struct hybbx_session *exclude); + +struct hybbx_circuit_hub; +struct hybbx_circuit_hub *hybbx_service_circuit_hub(hybbx_service_t *service); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SERVICE_H */ diff --git a/include/hybbx/session.h b/include/hybbx/session.h new file mode 100644 index 0000000..79f5343 --- /dev/null +++ b/include/hybbx/session.h @@ -0,0 +1,226 @@ +#ifndef HYBBX_SESSION_H +#define HYBBX_SESSION_H + +#include "hybbx/plugin.h" +#include "hybbx/storage.h" +#include "hybbx/mail.h" +#include "hybbx/proxymail.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; + +typedef enum hybbx_session_area { + HYBBX_AREA_MAIN = 1, + HYBBX_AREA_MAIL = 2, + HYBBX_AREA_CHAT = 3, + HYBBX_AREA_CONFERENCE = 4, + /** Inter-Main mail sub-area under mail (`/proxymail`, `/mail proxymail`). */ + HYBBX_AREA_PROXYMAIL = 5, + /** Inter-Main chat sub-area under chat (`/proxychat`, `/chat proxychat`). */ + HYBBX_AREA_PROXYCHAT = 6 +} hybbx_session_area_t; + +/** Maximum nested area depth (main + sub-areas). */ +#define HYBBX_AREA_STACK_MAX 8u + +/** + * Open a new connection session. + * When auto-login is enabled (default), assigns the next free Guest1 … Guest25 + * slot in memory (not written to user files). + * When auto-login is disabled, the session stays at a login prompt for + * registered `/login` and `/register` only (no guest slots). + */ +hybbx_result_t hybbx_session_open(struct hybbx_service *service, + const hybbx_transport_plugin_t *transport, + void *transport_data, + hybbx_session_t **out); + +void hybbx_session_close(hybbx_session_t *session); + +hybbx_result_t hybbx_session_handle_input(hybbx_session_t *session, + const uint8_t *data, size_t len); + +const char *hybbx_session_username(const hybbx_session_t *session); +/** User-facing name (nickname) for the logged-in account. */ +const char *hybbx_session_display_name(const hybbx_session_t *session); +/** + * Format `username@plugin` (transport name) into @p out. + * Uses display name and session transport / record. + */ +hybbx_result_t hybbx_session_format_user_at_plugin(const hybbx_session_t *session, + char *out, size_t out_len); +uint64_t hybbx_session_id(const hybbx_session_t *session); +const hybbx_session_record_t *hybbx_session_record(const hybbx_session_t *session); +/** Store the client peer address on the session record (for security.log). */ +hybbx_result_t hybbx_session_set_remote(hybbx_session_t *session, + const char *remote); +hybbx_user_level_t hybbx_session_user_level(const hybbx_session_t *session); +int hybbx_session_is_guest(const hybbx_session_t *session); +/** Non-zero for telnet, SSH, or WebSocket — not plugin/link transports. */ +int hybbx_session_is_interactive_user(const hybbx_session_t *session); + +/** + * Non-zero when this Sysop is hidden from /who and /online: + * [monitor] invisible-sysop=yes, or /monitor on. + * Non-Sysop grant users are never hidden. + */ +int hybbx_session_hidden_from_who(const hybbx_session_t *session); + +int hybbx_session_monitor_active(const hybbx_session_t *session); +void hybbx_session_set_monitor_active(hybbx_session_t *session, int on); +/** Non-zero when the session has completed login (guest or registered). */ +int hybbx_session_logged_in(const hybbx_session_t *session); +/** Non-zero when auto_login is off: banner + prompt, registered login only. */ +int hybbx_session_login_prompt(const hybbx_session_t *session); + +/** Write raw text to the connected client (no line ending, no prompt). */ +hybbx_result_t hybbx_session_write(hybbx_session_t *session, const char *text); + +/** Write a line and append CRLF. */ +hybbx_result_t hybbx_session_write_line(hybbx_session_t *session, + const char *text); + +/** One blank line before `/command` output (not chat/mail compose). */ +hybbx_result_t hybbx_session_command_gap(hybbx_session_t *session); + +/** + * Show the global input prompt when configured. + * Default (empty prompt): no output — users type on a blank line. + */ +hybbx_result_t hybbx_session_show_prompt(hybbx_session_t *session); + +/** Clear screen and discard the current input line; show prompt when set. */ +hybbx_result_t hybbx_session_clear_terminal(hybbx_session_t *session); + +/** Non-zero when typed characters are echoed back to the client. */ +int hybbx_session_input_echo(const hybbx_session_t *session); + +/** Enable or disable per-session input echo. */ +hybbx_result_t hybbx_session_set_input_echo(hybbx_session_t *session, int enabled); + +/** Current session area (main, mail, chat, …). */ +hybbx_session_area_t hybbx_session_area(const hybbx_session_t *session); + +/** Area name: main, mail, chat. */ +const char *hybbx_session_area_name(hybbx_session_area_t area); + +/** Parse area name (main, mail, chat). */ +hybbx_session_area_t hybbx_session_area_parse(const char *name); + +/** Go up one area level (alias: /back). Clears state for the area being left. */ +hybbx_result_t hybbx_session_leave_area(hybbx_session_t *session); + +/** Return to main from any depth (alias: /menu). Clears all sub-area state. */ +hybbx_result_t hybbx_session_go_main(hybbx_session_t *session); + +/** Enter an area (pushes onto the area stack). */ +hybbx_result_t hybbx_session_enter_area(hybbx_session_t *session, + hybbx_session_area_t area); + +/** Switch the session to a registered (non-guest) account after login. */ +hybbx_result_t hybbx_session_switch_user(hybbx_session_t *session, + const hybbx_user_record_t *user); + +/** + * Periodic session maintenance (guest time limit, etc.). + * @return HYBBX_SESSION_END when the session should be closed. + */ +hybbx_result_t hybbx_session_tick(hybbx_session_t *session); + +/** Join a chat channel (enters chat area). @p channel_index is 1-based. */ +hybbx_result_t hybbx_session_join_chat_channel(hybbx_session_t *session, + unsigned channel_index); + +/** Current chat channel (1-based), or 0 when not in chat. */ +unsigned hybbx_session_chat_channel(const hybbx_session_t *session); + +struct hybbx_service *hybbx_session_service(hybbx_session_t *session); + +/** Join a private conference (conference area). Internal — use hybbx_conference_start. */ +hybbx_result_t hybbx_session_join_conference(hybbx_session_t *session, + const char *topic, + const char *partner_username); + +void hybbx_session_clear_conference(hybbx_session_t *session); + +int hybbx_session_conference_may_invite(hybbx_session_t *session, + const char *target); +void hybbx_session_conference_invite_sent(hybbx_session_t *session, + const char *target); +void hybbx_session_set_conference_invite(hybbx_session_t *session, + const char *from_username, + const char *topic); +/** Optional deadline (0 = none). Used by monitor-initiated invites. */ +void hybbx_session_set_conference_invite_deadline(hybbx_session_t *session, + time_t deadline); +time_t hybbx_session_conference_invite_deadline(const hybbx_session_t *session); +void hybbx_session_clear_conference_invite(hybbx_session_t *session); +const char *hybbx_session_conference_invite_from(const hybbx_session_t *session); +const char *hybbx_session_conference_invite_topic(const hybbx_session_t *session); +const char *hybbx_session_conference_partner(const hybbx_session_t *session); + +/** Enter local mail area (does not enter proxymail). */ +hybbx_result_t hybbx_session_enter_mail(hybbx_session_t *session); + +/** Enter proxymail sub-area (pushes mail when needed). */ +hybbx_result_t hybbx_session_enter_proxymail(hybbx_session_t *session); + +/** Enter local chat area without joining a channel. */ +hybbx_result_t hybbx_session_enter_chat(hybbx_session_t *session); + +/** Enter proxychat sub-area (pushes chat when needed). */ +hybbx_result_t hybbx_session_enter_proxychat(hybbx_session_t *session); + +/** Non-zero when composing an outbound mail message. */ +int hybbx_session_mail_composing(const hybbx_session_t *session); + +/** Start mail compose to @p to_user with @p subject; enters mail area. */ +hybbx_result_t hybbx_session_mail_compose_start(hybbx_session_t *session, + const char *to_user, + const char *subject); + +/** Cancel an in-progress mail compose. */ +void hybbx_session_mail_compose_cancel(hybbx_session_t *session); + +/** Body accumulated so far during compose (NUL-terminated). */ +const char *hybbx_session_mail_compose_body(const hybbx_session_t *session); + +const char *hybbx_session_mail_compose_to(const hybbx_session_t *session); +const char *hybbx_session_mail_compose_subject(const hybbx_session_t *session); + +/** Non-zero when composing proxymail (`user@remote-main` recipient). */ +int hybbx_session_proxymail_composing(const hybbx_session_t *session); + +hybbx_result_t hybbx_session_proxymail_compose_start(hybbx_session_t *session, + const char *to_address, + const char *subject); + +void hybbx_session_proxymail_compose_cancel(hybbx_session_t *session); + +const char *hybbx_session_proxymail_compose_body(const hybbx_session_t *session); +const char *hybbx_session_proxymail_compose_to(const hybbx_session_t *session); +const char *hybbx_session_proxymail_compose_subject( + const hybbx_session_t *session); + +/** Wall-clock time when this session connected (for bandwidth policy ordering). */ +time_t hybbx_session_connected_at(const hybbx_session_t *session); + +/** Non-zero when the session is frozen by circuit bandwidth limits. */ +int hybbx_session_bandwidth_paused(const hybbx_session_t *session); + +void hybbx_session_set_bandwidth_paused(hybbx_session_t *session, int paused); + +/** + * End the session after sending @ref HYBBX_BANDWIDTH_DISCONNECT_MSG. + * Newest-first under low-bandwidth link pressure. + */ +void hybbx_session_disconnect_bandwidth(hybbx_session_t *session); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SESSION_H */ diff --git a/include/hybbx/socket.h b/include/hybbx/socket.h new file mode 100644 index 0000000..1baa639 --- /dev/null +++ b/include/hybbx/socket.h @@ -0,0 +1,35 @@ +#ifndef HYBBX_SOCKET_H +#define HYBBX_SOCKET_H + +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include + +#ifndef MSG_NOSIGNAL +#define MSG_NOSIGNAL 0 +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** Suppress SIGPIPE on send() (SO_NOSIGPIPE on BSD/MacOS; noop on Linux). */ +void hybbx_socket_nosigpipe(int fd); + +/** + * Format the peer IP address of a connected socket into @p buf. + * IPv4 dotted-quad or IPv6 colon form (no brackets). + */ +hybbx_result_t hybbx_socket_peer_name(int fd, char *buf, size_t buf_len); + +/** Log bind(2) failure; hints when @c EADDRINUSE (HyBBX already running). */ +void hybbx_socket_log_bind_failure(const char *component, const char *addr, + unsigned port); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SOCKET_H */ diff --git a/include/hybbx/ssh.h b/include/hybbx/ssh.h new file mode 100644 index 0000000..2555198 --- /dev/null +++ b/include/hybbx/ssh.h @@ -0,0 +1,57 @@ +#ifndef HYBBX_SSH_H +#define HYBBX_SSH_H + +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default HyBBX SSH TCP port (non-privileged; parallels telnet 2323). */ +#define HYBBX_SSH_DEFAULT_PORT 3232u + +#define HYBBX_SSH_BIND_V4_MAX 64 +#define HYBBX_SSH_BIND_V6_MAX 64 +#define HYBBX_SSH_HOSTKEY_DIR_MAX HYBBX_PATH_MAX + +#define HYBBX_SSH_DEFAULT_BIND_V4 "0.0.0.0" +#define HYBBX_SSH_DEFAULT_BIND_V6 "::" +#define HYBBX_SSH_DEFAULT_HOSTKEY_DIR "keys" + +#define HYBBX_SSH_HOSTKEY_ED25519 "ssh_host_ed25519_key" + +/** Regenerate Ed25519 host keys when older than this (days). */ +#define HYBBX_SSH_HOSTKEY_VALID_DAYS 1825u + +typedef struct hybbx_ssh_config { + char bind_v4[HYBBX_SSH_BIND_V4_MAX]; + char bind_v6[HYBBX_SSH_BIND_V6_MAX]; + char hostkey_dir[HYBBX_SSH_HOSTKEY_DIR_MAX]; + unsigned int port; + int ipv4; + int ipv6; +} hybbx_ssh_config_t; + +void hybbx_ssh_config_defaults(hybbx_ssh_config_t *config); + +/** + * Parse a semicolon-separated key=value transport config string + * (from transport.ssh INI section). + */ +hybbx_result_t hybbx_ssh_config_parse(const char *config, + hybbx_ssh_config_t *out); + +/** + * Ensure Ed25519 host keys exist under @p keys_dir (resolved path). + * Writes the private key path into @p hostkey_path. + */ +hybbx_result_t hybbx_ssh_keys_ensure(const char *keys_dir, + char *hostkey_path, + size_t hostkey_path_len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SSH_H */ diff --git a/include/hybbx/storage.h b/include/hybbx/storage.h new file mode 100644 index 0000000..f4b4802 --- /dev/null +++ b/include/hybbx/storage.h @@ -0,0 +1,178 @@ +#ifndef HYBBX_STORAGE_H +#define HYBBX_STORAGE_H + +#include "hybbx/auth.h" +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum hybbx_storage_backend_kind { + HYBBX_STORAGE_FLATFILE = 1, + HYBBX_STORAGE_SQLITE = 2, + HYBBX_STORAGE_MYSQL = 3, + HYBBX_STORAGE_MARIADB = 4 +} hybbx_storage_backend_kind_t; + +#define HYBBX_USER_NAME_MAX 64 +#define HYBBX_USER_NICKNAME_MAX 64 +#define HYBBX_USER_FULL_NAME_MAX 128 +#define HYBBX_USER_COUNTRY_MAX 64 +#define HYBBX_USER_LOCATION_MAX 128 +#define HYBBX_USER_EMAIL_MAX 128 +#define HYBBX_USER_PASSWORD_MAX HYBBX_PASSWORD_STORED_MAX +#define HYBBX_TRANSPORT_NAME_MAX 32 +/** Peer address string (IPv4 or bracketed IPv6). */ +#define HYBBX_REMOTE_ADDR_MAX 64 + +/** Maximum registered user records per INI user-file shard. */ +#define HYBBX_USERS_PER_FILE 50u + +/** INI section prefix for one user: [user.]. */ +#define HYBBX_USER_INI_SECTION_PREFIX "user." + +typedef struct hybbx_user_registration { + char username[HYBBX_USER_NAME_MAX]; + char nickname[HYBBX_USER_NICKNAME_MAX]; + char full_name[HYBBX_USER_FULL_NAME_MAX]; + char country[HYBBX_USER_COUNTRY_MAX]; + char location[HYBBX_USER_LOCATION_MAX]; + char email[HYBBX_USER_EMAIL_MAX]; + char password[HYBBX_USER_PASSWORD_MAX]; +} hybbx_user_registration_t; + +/** Validate profile fields only (full name, country, location, email). */ +int hybbx_user_profile_valid(const hybbx_user_registration_t *reg); + +/** Validate registration profile fields (username + full name, country, location, email). */ +int hybbx_registration_valid(const hybbx_user_registration_t *reg, + const char *guest_prefix); + +typedef struct hybbx_user_record { + uint64_t id; + char username[HYBBX_USER_NAME_MAX]; + char nickname[HYBBX_USER_NICKNAME_MAX]; + hybbx_user_level_t level; + int active; + time_t created_at; + char full_name[HYBBX_USER_FULL_NAME_MAX]; + char country[HYBBX_USER_COUNTRY_MAX]; + char location[HYBBX_USER_LOCATION_MAX]; + char email[HYBBX_USER_EMAIL_MAX]; + char password[HYBBX_USER_PASSWORD_MAX]; + time_t last_login_at; +} hybbx_user_record_t; + +typedef struct hybbx_session_record { + uint64_t session_id; + uint64_t user_id; + char username[HYBBX_USER_NAME_MAX]; + char transport[HYBBX_TRANSPORT_NAME_MAX]; + char remote[HYBBX_REMOTE_ADDR_MAX]; + time_t connected_at; + time_t disconnected_at; + int active; +} hybbx_session_record_t; + +typedef struct hybbx_storage hybbx_storage_t; + +/** Paths and backup settings for SQLite (`[storage]`). */ +typedef struct hybbx_storage_sql_config { + char user_db[HYBBX_PATH_MAX]; + char mail_db[HYBBX_PATH_MAX]; + unsigned backup_interval_sec; + char backup_path[HYBBX_PATH_MAX]; +} hybbx_storage_sql_config_t; + +typedef struct hybbx_storage_options { + hybbx_storage_backend_kind_t backend; + const char *path; + const char *guest_prefix; + const hybbx_storage_sql_config_t *sql_cfg; +} hybbx_storage_options_t; + +struct hybbx_config; + +void hybbx_storage_sql_config_defaults(hybbx_storage_sql_config_t *cfg); + +/** Resolve `user_db` / `mail_db` under @p storage_path from INI `[storage]`. */ +void hybbx_storage_sql_config_apply(hybbx_storage_sql_config_t *cfg, + const struct hybbx_config *config, + const char *storage_path); + +const hybbx_storage_sql_config_t *hybbx_storage_sql_config( + const hybbx_storage_t *storage); + +/** Copy SQLite DB files to fallback paths (no-op when not SQLite). */ +void hybbx_storage_backup_tick(hybbx_storage_t *storage); + +hybbx_storage_t *hybbx_storage_open(const hybbx_storage_options_t *options); +void hybbx_storage_close(hybbx_storage_t *storage); + +hybbx_storage_backend_kind_t hybbx_storage_backend(const hybbx_storage_t *storage); + +/** Root data directory path, or NULL when unset. */ +const char *hybbx_storage_root_path(const hybbx_storage_t *storage); + +/** + * Register a new user account (level user, inactive until Sysop/Admin activates). + */ +hybbx_result_t hybbx_storage_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out); + +/** Look up a user by login name (case-insensitive; stored lowercase). */ +hybbx_result_t hybbx_storage_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out); + +/** Resolve by login name or nickname (case-insensitive). */ +hybbx_result_t hybbx_storage_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out); + +/** Count existing accounts at a given level (e.g. enforce one Sysop). */ +hybbx_result_t hybbx_storage_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count); + +/** + * Iterate all user records. @p fn returns HYBBX_OK to continue; any other + * result stops iteration and is returned from this function. + */ +typedef hybbx_result_t (*hybbx_storage_user_fn)(const hybbx_user_record_t *user, + void *ctx); + +hybbx_result_t hybbx_storage_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx); + +hybbx_result_t hybbx_storage_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out); + +hybbx_result_t hybbx_storage_session_end(hybbx_storage_t *storage, + uint64_t session_id); + +/** + * Replace an existing user record (matched by @p user->id). + */ +hybbx_result_t hybbx_storage_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user); + +/** Remove a user record from storage (matched by @p user_id). */ +hybbx_result_t hybbx_storage_delete_user(hybbx_storage_t *storage, + uint64_t user_id); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_STORAGE_H */ diff --git a/include/hybbx/telnet.h b/include/hybbx/telnet.h new file mode 100644 index 0000000..956f1f5 --- /dev/null +++ b/include/hybbx/telnet.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_TELNET_H +#define HYBBX_TELNET_H + +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default HyBBX telnet TCP port (BBX / terminal convention). */ +#define HYBBX_TELNET_DEFAULT_PORT 2323u + +#define HYBBX_TELNET_BIND_V4_MAX 64 +#define HYBBX_TELNET_BIND_V6_MAX 64 + +#define HYBBX_TELNET_DEFAULT_BIND_V4 "0.0.0.0" +#define HYBBX_TELNET_DEFAULT_BIND_V6 "::" + +typedef struct hybbx_telnet_config { + char bind_v4[HYBBX_TELNET_BIND_V4_MAX]; + char bind_v6[HYBBX_TELNET_BIND_V6_MAX]; + unsigned int port; + int ipv4; + int ipv6; +} hybbx_telnet_config_t; + +void hybbx_telnet_config_defaults(hybbx_telnet_config_t *config); + +/** + * Parse a semicolon-separated key=value transport config string + * (from transport.telnet INI section). + */ +hybbx_result_t hybbx_telnet_config_parse(const char *config, + hybbx_telnet_config_t *out); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TELNET_H */ diff --git a/include/hybbx/terminal.h b/include/hybbx/terminal.h new file mode 100644 index 0000000..fec0871 --- /dev/null +++ b/include/hybbx/terminal.h @@ -0,0 +1,39 @@ +#ifndef HYBBX_TERMINAL_H +#define HYBBX_TERMINAL_H + +#include "hybbx/types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_session; + +/** + * HyBBX uses a single terminal palette: light gray text on a black background. + * ANSI SGR: foreground 37, background 40. + */ +#define HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK "\033[37;40m" + +/** Clear screen and move cursor home (requires `traffic.ansi = yes`). */ +#define HYBBX_TERM_CLEAR_SCREEN "\033[2J\033[H" + +/** Apply the HyBBX terminal colors on a session (sent once at connect). */ +hybbx_result_t hybbx_term_init_session(struct hybbx_session *session); + +/** Clear the client screen (ANSI or plain newlines). */ +hybbx_result_t hybbx_term_clear_screen(struct hybbx_session *session); + +/** + * Copy @p src to @p dst without ANSI escape sequences (colors, cursor, etc.). + * @return length of written string excluding the terminator. + */ +size_t hybbx_term_copy_plain(const char *src, char *dst, size_t dst_size); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TERMINAL_H */ diff --git a/include/hybbx/texts.h b/include/hybbx/texts.h new file mode 100644 index 0000000..ef1b9e3 --- /dev/null +++ b/include/hybbx/texts.h @@ -0,0 +1,84 @@ +#ifndef HYBBX_TEXTS_H +#define HYBBX_TEXTS_H + +#include "hybbx/types.h" +#include "hybbx/limits.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_session; + +#define HYBBX_DEFAULT_TEXTS_PATH HYBBX_DIR_TEXT + +#define HYBBX_TEXT_BANNER "banner.txt" +#define HYBBX_TEXT_MOTD "motd.txt" +#define HYBBX_TEXT_NEWS "news.txt" +#define HYBBX_TEXT_RULES "rules.txt" +#define HYBBX_TEXT_VERSION "version.txt" + +/** Substituted in banner.txt / version.txt — HyBBX version number (e.g. 2.4.0). */ +#define HYBBX_BANNER_TOKEN_VERSION "@version@" +/** Substituted in banner.txt line 2. */ +#define HYBBX_BANNER_TOKEN_SERVICE "@service@" +/** Substituted in motd.txt (current session nickname). */ +#define HYBBX_TEXT_TOKEN_USERNAME "@username@" +/** Substituted in version.txt — host operating system name (e.g. Linux). */ +#define HYBBX_TEXT_TOKEN_OS "@os@" +/** Substituted in text files — local time per `[time]` (default HH:MM:SS). */ +#define HYBBX_TEXT_TOKEN_TIME "%time%" +/** Substituted in text files — local date per `[time] date=` (default YYYY/MM/DD). */ +#define HYBBX_TEXT_TOKEN_DATE "%date%" + +#define HYBBX_TEXTS_PATH_MAX 512 + +typedef struct hybbx_texts_config { + char path[HYBBX_TEXTS_PATH_MAX]; +} hybbx_texts_config_t; + +void hybbx_texts_config_defaults(hybbx_texts_config_t *texts); + +/** + * Build full path to a text file under the configured texts directory. + * Returns HYBBX_OK on success. + */ +hybbx_result_t hybbx_texts_resolve(const hybbx_texts_config_t *texts, + const char *filename, + char *out, size_t out_len); + +/** + * Send a text file line-by-line to the session (connection output). + * Expands @version@, @service@, @username@, @os@, %time%, and %date%. + * Missing files are skipped silently. + */ +hybbx_result_t hybbx_texts_send_file(const hybbx_texts_config_t *texts, + struct hybbx_session *session, + const char *filename); + +/** + * Send banner.txt with @version@ and @service@ tokens expanded. + */ +hybbx_result_t hybbx_texts_send_banner(const hybbx_texts_config_t *texts, + struct hybbx_session *session, + const char *version, + const char *service_name); + +/** + * Send motd.txt with @username@ expanded from @p session. + */ +hybbx_result_t hybbx_texts_send_motd(const hybbx_texts_config_t *texts, + struct hybbx_session *session); + +/** + * Send version.txt with @version@ and @os@ expanded from this host. + * Falls back to the built-in two-line version block if the file is missing. + */ +hybbx_result_t hybbx_texts_send_version(const hybbx_texts_config_t *texts, + struct hybbx_session *session); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TEXTS_H */ diff --git a/include/hybbx/tnc.h b/include/hybbx/tnc.h new file mode 100644 index 0000000..9655aa8 --- /dev/null +++ b/include/hybbx/tnc.h @@ -0,0 +1,240 @@ +#ifndef HYBBX_TNC_H +#define HYBBX_TNC_H + +/** + * Unified TNC / AX.25 link driver for HyBBX packet radio. + * + * Supported hardware profiles (see docs/TNCS.md): + * - Landolt TNC2C, classic TNC-2 / PK-TNC2 (TheFirmware) + * - AEA PK-232, MFJ-1278, Kantronics KPC + * - Generic TNC2-class controllers + * + * Not in packet_radio: BayCom/based SER12 / PC-COM TCM3105 — use MAX25 + * bcpr (`max25e0`) then optional HyBBX baycom transport or [max25] attach. + * UN1TME = TNCs only. + * + * Host protocols: KISS, TNC2 host mode, 6PACK (DF6BU). + */ + +#include "hybbx/ax25.h" +#include "hybbx/packet_radio.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_TNC2C_DEFAULT_HOST_BAUD HYBBX_PACKET_RADIO_DEFAULT_BAUD +#define HYBBX_TNC2C_DEFAULT_RADIO_BAUD 1200u +#define HYBBX_AFSK_RADIO_BAUD 1200u +#define HYBBX_G3RUH_RADIO_BAUD 9600u +#define HYBBX_TNC2C_CLOCK_4_9_MHZ 4.9f +#define HYBBX_TNC2C_CLOCK_9_8_MHZ 9.8f +#define HYBBX_TNC2C_CLOCK_10_MHZ 10.0f +#define HYBBX_TNC2C_HOST_BAUD_MIN 300u +#define HYBBX_TNC2C_HOST_BAUD_MAX 38400u +#define HYBBX_TNC2C_DATA_BITS 7u + +typedef enum hybbx_tnc_serial_parity { + HYBBX_TNC_SERIAL_PARITY_UNSET = 0, + HYBBX_TNC_SERIAL_PARITY_NONE = 1, + HYBBX_TNC_SERIAL_PARITY_EVEN = 2, + HYBBX_TNC_SERIAL_PARITY_ODD = 3 +} hybbx_tnc_serial_parity_t; + +#define HYBBX_PACKET_RADIO_VIA_MAX 8 + +typedef enum hybbx_tnc2c_modem { + HYBBX_TNC2C_MODEM_TCM3105 = 1, + HYBBX_TNC2C_MODEM_9600 = 2, /**< G3RUH-compatible 9600 baud FSK */ + HYBBX_TNC2C_MODEM_19200 = 3 +} hybbx_tnc2c_modem_t; + +/** + * Over-the-air modulation standard. + * AFSK (Bell 202 / TCM3105 @ 1200) is the classic CB packet default. + * G3RUH FSK @ 9600 baud for higher throughput (requires compatible modem/TNC). + */ +typedef enum hybbx_packet_radio_modulation { + HYBBX_PACKET_RADIO_MOD_UNSET = 0, + HYBBX_PACKET_RADIO_MOD_AFSK = 1, + HYBBX_PACKET_RADIO_MOD_G3RUH_FSK = 2 +} hybbx_packet_radio_modulation_t; + +#define HYBBX_PACKET_RADIO_MOD_DEFAULT HYBBX_PACKET_RADIO_MOD_AFSK + +typedef enum hybbx_packet_radio_protocol { + HYBBX_PACKET_RADIO_PROTO_KISS = 1, + HYBBX_PACKET_RADIO_PROTO_HOSTMODE = 2, + HYBBX_PACKET_RADIO_PROTO_SIXPACK = 3 +} hybbx_packet_radio_protocol_t; + +typedef enum hybbx_packet_radio_tnc { + HYBBX_PACKET_RADIO_TNC_TNC2C = 1, + HYBBX_PACKET_RADIO_TNC_BAYCOM = 2, + HYBBX_PACKET_RADIO_TNC_PCCOM = 3, + HYBBX_PACKET_RADIO_TNC_GENERIC = 4, + HYBBX_PACKET_RADIO_TNC_TNC2 = 5, + HYBBX_PACKET_RADIO_TNC_PK232 = 6, + HYBBX_PACKET_RADIO_TNC_MFJ1278 = 7, + HYBBX_PACKET_RADIO_TNC_KANTRONICS = 8 +} hybbx_packet_radio_tnc_t; + +/** How HyBBX enters KISS mode on the host serial link. */ +typedef enum hybbx_kiss_entry { + HYBBX_KISS_ENTRY_UNSET = 0, + HYBBX_KISS_ENTRY_NONE = 1, + HYBBX_KISS_ENTRY_KISS_ON = 2, + HYBBX_KISS_ENTRY_ESC_AT_K = 3, + HYBBX_KISS_ENTRY_AUTO = 4 +} hybbx_kiss_entry_t; + +/** How HyBBX leaves KISS mode on shutdown. */ +typedef enum hybbx_kiss_exit { + HYBBX_KISS_EXIT_UNSET = 0, + HYBBX_KISS_EXIT_NONE = 1, + HYBBX_KISS_EXIT_KISS_OFF = 2, + HYBBX_KISS_EXIT_KISS_FRAME = 3, + HYBBX_KISS_EXIT_AUTO = 4 +} hybbx_kiss_exit_t; + +/** + * RF service band — drives safe duplex defaults. + * CB is half-duplex only (shared channel). Amateur may use full-duplex. + */ +typedef enum hybbx_packet_radio_band { + HYBBX_PACKET_RADIO_BAND_UNSET = 0, + HYBBX_PACKET_RADIO_BAND_CB = 1, + HYBBX_PACKET_RADIO_BAND_AMATEUR = 2 +} hybbx_packet_radio_band_t; + +/** + * Radio link duplex mode (distinct from host serial full-duplex). + * half = one direction at a time on the RF channel (CB / classic packet). + * full = TNC may key TX while receiving (amateur split / full-dup setups). + */ +typedef enum hybbx_packet_radio_duplex { + HYBBX_PACKET_RADIO_DUPLEX_UNSET = 0, + HYBBX_PACKET_RADIO_DUPLEX_HALF = 1, + HYBBX_PACKET_RADIO_DUPLEX_FULL = 2 +} hybbx_packet_radio_duplex_t; + +#define HYBBX_PACKET_RADIO_DUPLEX_DEFAULT HYBBX_PACKET_RADIO_DUPLEX_HALF + +typedef struct hybbx_tnc_params { + float clock_mhz; + hybbx_tnc2c_modem_t modem; + hybbx_packet_radio_modulation_t modulation; + unsigned int radio_baud; + unsigned int kiss_port; + unsigned int txdelay; + unsigned int persist; + unsigned int slot; + unsigned int txtail; + hybbx_packet_radio_band_t band; + hybbx_packet_radio_duplex_t duplex; + int fullduplex; + hybbx_kiss_entry_t kiss_entry; + hybbx_kiss_exit_t kiss_exit; + int host_connect_on_start; + unsigned int data_bits; + hybbx_tnc_serial_parity_t serial_parity; + unsigned int stop_bits; + int assert_modem_lines; +} hybbx_tnc_params_t; + +typedef struct hybbx_packet_radio_config { + hybbx_packet_radio_device_type_t device_type; + char *device; + unsigned int baud; + hybbx_packet_radio_tnc_t tnc; + hybbx_packet_radio_protocol_t protocol; + hybbx_tnc_params_t params; + char *mycall; + char *dest_call; + char *via[HYBBX_PACKET_RADIO_VIA_MAX]; + unsigned via_count; + char *circuit_host; + unsigned circuit_port; + char *link_id; + char *link_password; + char *link_role; + char *frequency_mhz; +} hybbx_packet_radio_config_t; + +typedef struct hybbx_tnc hybbx_tnc_t; + +typedef void (*hybbx_tnc_ui_cb)(const uint8_t *payload, size_t len, + const hybbx_ax25_path_t *path, + void *userdata); + +/** Raw AX.25 frame from KISS/6PACK (before UI parsing). */ +typedef void (*hybbx_tnc_frame_cb)(const uint8_t *frame, size_t len, + void *userdata); + +hybbx_result_t hybbx_tnc_open(hybbx_tnc_t **out, + const hybbx_packet_radio_config_t *config, + hybbx_tnc_frame_cb frame_cb, + hybbx_tnc_ui_cb ui_cb, + void *rx_userdata); + +void hybbx_tnc_close(hybbx_tnc_t *tnc); + +hybbx_result_t hybbx_tnc_poll(hybbx_tnc_t *tnc); + +/** POSIX serial fd for poll/select; returns -1 when unavailable. */ +int hybbx_tnc_serial_fd(const hybbx_tnc_t *tnc); + +/** Send a raw AX.25 frame (already built) to the TNC. */ +hybbx_result_t hybbx_tnc_send_frame(hybbx_tnc_t *tnc, + const uint8_t *frame, size_t len); + +/** Build and send an AX.25 UI frame from text payload. */ +hybbx_result_t hybbx_tnc_send_ui(hybbx_tnc_t *tnc, + const uint8_t *payload, size_t len); + +/** Send bytes during host-mode connected converse. */ +hybbx_result_t hybbx_tnc_send_converse(hybbx_tnc_t *tnc, + const char *data, size_t len); + +int hybbx_tnc_host_connected(const hybbx_tnc_t *tnc); + +hybbx_packet_radio_duplex_t hybbx_tnc_duplex(const hybbx_tnc_t *tnc); +hybbx_packet_radio_band_t hybbx_tnc_band(const hybbx_tnc_t *tnc); +int hybbx_tnc_duplex_is_full(const hybbx_tnc_t *tnc); + +const char *hybbx_packet_radio_duplex_name(hybbx_packet_radio_duplex_t duplex); +const char *hybbx_packet_radio_band_name(hybbx_packet_radio_band_t band); + +const char *hybbx_packet_radio_modulation_name( + hybbx_packet_radio_modulation_t modulation); + +int hybbx_packet_radio_modulation_is_g3ruh( + hybbx_packet_radio_modulation_t modulation); + +/** Apply g3ruh_fsk / modulation keys to modem, radio_baud, and timing. */ +hybbx_result_t hybbx_tnc_finalize_modulation(hybbx_packet_radio_config_t *cfg); + +/** Apply band/duplex rules and sync @p fullduplex for the TNC firmware. */ +hybbx_result_t hybbx_tnc_finalize_radio_duplex(hybbx_packet_radio_config_t *cfg); + +/** Apply profile-specific defaults (TNC2C, generic, …). */ +hybbx_result_t hybbx_tnc_profile_apply_defaults(hybbx_packet_radio_config_t *cfg); + +/** Apply host serial line defaults (TNC2C: 7E1 + RTS/DTR). */ +hybbx_result_t hybbx_tnc_finalize_serial_line(hybbx_packet_radio_config_t *cfg); + +/** Apply kiss_entry / kiss_exit defaults from TNC profile when unset. */ +hybbx_result_t hybbx_tnc_finalize_kiss_entry(hybbx_packet_radio_config_t *cfg); + +/** Apply CSMA defaults (Persist/SlotTime) from band when unset in INI. */ +hybbx_result_t hybbx_tnc_finalize_csma(hybbx_packet_radio_config_t *cfg); + +const char *hybbx_packet_radio_tnc_name(hybbx_packet_radio_tnc_t tnc); +const char *hybbx_kiss_entry_name(hybbx_kiss_entry_t entry); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TNC_H */ diff --git a/include/hybbx/tnc2c.h b/include/hybbx/tnc2c.h new file mode 100644 index 0000000..42f5277 --- /dev/null +++ b/include/hybbx/tnc2c.h @@ -0,0 +1,27 @@ +#ifndef HYBBX_TNC2C_H +#define HYBBX_TNC2C_H + +/** + * Backward-compatible header for the Landolt TNC2C driver. + * New code should include hybbx/tnc.h. + */ + +#include "hybbx/tnc.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef hybbx_tnc_t hybbx_tnc2c_t; +typedef hybbx_tnc_params_t hybbx_tnc2c_params_t; + +/* hybbx_tnc2c_open removed — use hybbx_tnc_open() with RX callback. */ +#define hybbx_tnc2c_close hybbx_tnc_close +#define hybbx_tnc2c_poll hybbx_tnc_poll +#define hybbx_tnc2c_send_frame hybbx_tnc_send_frame + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TNC2C_H */ diff --git a/include/hybbx/traffic.h b/include/hybbx/traffic.h new file mode 100644 index 0000000..8d06cf3 --- /dev/null +++ b/include/hybbx/traffic.h @@ -0,0 +1,59 @@ +#ifndef HYBBX_TRAFFIC_H +#define HYBBX_TRAFFIC_H + +#include "hybbx/types.h" + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_config; +struct hybbx_session; + +/** Host link speed HyBBX is optimized for (8N1, ~1 byte per 10 bit times). */ +#define HYBBX_BAUD2400 2400u + +/** Default terminal width (columns). */ +#define HYBBX_LINE_WIDTH 80u + +/** Maximum configurable `line_width` in INI. */ +#define HYBBX_LINE_WIDTH_MAX 132u + +/** Maximum user input line (commands / chat). */ +#define HYBBX_LINE_MAX 80u + +/** Typical AX.25 UI payload ceiling used for outbound chunk sizing. */ +#define HYBBX_AX25_PAYLOAD_MAX 256u + +typedef struct hybbx_traffic_config { + unsigned baud; + unsigned line_width; + int pace_output; + int ansi; + int input_echo; +} hybbx_traffic_config_t; + +void hybbx_traffic_config_defaults(hybbx_traffic_config_t *cfg); +void hybbx_traffic_config_apply(const struct hybbx_config *config); +const hybbx_traffic_config_t *hybbx_traffic_config_get(void); + +/** + * Microseconds to wait after one 8N1 byte at @p baud (0 when pacing is off). + */ +unsigned hybbx_traffic_byte_delay_us(unsigned baud); + +/** + * Emit @p data to @p session with plain-ASCII policy, column wrap, and pacing. + * @p out_col is updated (wrap position on the current output line). + */ +hybbx_result_t hybbx_traffic_emit(struct hybbx_session *session, + unsigned *out_col, + const char *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TRAFFIC_H */ diff --git a/include/hybbx/types.h b/include/hybbx/types.h new file mode 100644 index 0000000..775a711 --- /dev/null +++ b/include/hybbx/types.h @@ -0,0 +1,42 @@ +#ifndef HYBBX_TYPES_H +#define HYBBX_TYPES_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum hybbx_result { + HYBBX_OK = 0, + /** Input is a local/mailbox command (not a HyBBX '/' command). */ + HYBBX_LOCAL_CMD = 1, + /** Session should be closed (e.g. /exit). */ + HYBBX_SESSION_END = 2, + HYBBX_ERR_INVALID = -1, + HYBBX_ERR_NOMEM = -2, + HYBBX_ERR_NOT_FOUND = -3, + HYBBX_ERR_IO = -4, + HYBBX_ERR_UNSUPPORTED = -5, + HYBBX_ERR_BUSY = -6, + HYBBX_ERR_DENIED = -7 +} hybbx_result_t; + +typedef enum hybbx_transport_kind { + HYBBX_TRANSPORT_TELNET = 1, + HYBBX_TRANSPORT_PACKET_RADIO = 2, + HYBBX_TRANSPORT_CIRCUIT = 3, + HYBBX_TRANSPORT_ARDOP = 4, + HYBBX_TRANSPORT_CRDOP = 5, + HYBBX_TRANSPORT_SSH = 6, + HYBBX_TRANSPORT_WEBSOCKET = 7, + HYBBX_TRANSPORT_BAYCOM = 8, + HYBBX_TRANSPORT_MAINS_PROXY = 9, +} hybbx_transport_kind_t; + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TYPES_H */ diff --git a/include/hybbx/util.h b/include/hybbx/util.h new file mode 100644 index 0000000..de439b4 --- /dev/null +++ b/include/hybbx/util.h @@ -0,0 +1,143 @@ +#ifndef HYBBX_UTIL_H +#define HYBBX_UTIL_H + +#include "hybbx/types.h" + +#include +#include + +struct tm; + +struct hybbx_config; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Safe string copy (always NUL-terminates when @p dst_size > 0). + * Returns bytes copied excluding the terminator. + */ +size_t hybbx_strlcpy(char *dst, const char *src, size_t dst_size); + +/** + * Join @p base and @p name into @p out with a single '/'. + * Rejects empty components and path traversal (".."). + */ +hybbx_result_t hybbx_path_join(char *out, size_t out_len, + const char *base, const char *name); + +/** Default relative storage path (@ref HYBBX_DIR_DATA). */ +hybbx_result_t hybbx_default_user_data_path(char *out, size_t out_len); + +/** + * Expand a config path: `~` → $HOME, `~/foo` → $HOME/foo. + * Empty or NULL @p path → @ref HYBBX_DIR_DATA. Other paths copied unchanged. + */ +hybbx_result_t hybbx_path_expand(char *out, size_t out_len, const char *path); + +/** Set the HyBBX install root used by @ref hybbx_path_resolve. */ +void hybbx_install_root_set(const char *root); + +/** Install root set by @ref hybbx_install_root_set, or NULL when unset. */ +const char *hybbx_install_root_get(void); + +/** + * Resolve a config path: @ref hybbx_path_expand, then prefix relative paths + * with the install root when set. + */ +hybbx_result_t hybbx_path_resolve(char *out, size_t out_len, const char *path); + +/** Parent directory of @p path (POSIX `/` rules). */ +hybbx_result_t hybbx_path_dirname(const char *path, char *out, size_t out_len); + +/** + * Host operating system name for display (e.g. Linux, FreeBSD, MacOS, Windows). + * Does not include OS version or kernel release. + */ +hybbx_result_t hybbx_platform_os_name(char *out, size_t out_len); + +/** Return non-zero when @p len is safe for HyBBX allocations. */ +int hybbx_size_ok(size_t len); + +/** + * HyBBX boolean configuration standard. + * Canonical written form: @c yes / @c no (see @ref HYBBX_BOOL_YES / @ref HYBBX_BOOL_NO). + * Accepted true: yes, true, enable, enabled, on, 1 + * Accepted false: no, false, disable, disabled, off, 0 + * Matching is case-insensitive. + */ +#define HYBBX_BOOL_YES "yes" +#define HYBBX_BOOL_NO "no" + +/** Return non-zero when @p value is a recognized true token. */ +int hybbx_bool_is_true(const char *value); + +/** Return non-zero when @p value is a recognized false token. */ +int hybbx_bool_is_false(const char *value); + +/** + * Parse a boolean string. Returns 1 or 0 when recognized; otherwise + * returns @p default_value (also used when @p value is NULL or empty). + */ +int hybbx_parse_bool(const char *value, int default_value); + +/** Canonical @c yes / @c no string for a boolean value. */ +const char *hybbx_bool_to_string(int value); + +/** Short name for @p rc (logging / tests). */ +const char *hybbx_result_name(hybbx_result_t rc); + +/** + * HyBBX system-local date/time formatting for logs and text/ tokens. + * Default: 24-hour clock with seconds, ISO date YYYY/MM/DD. + */ +typedef enum hybbx_date_format { + HYBBX_DATE_ISO = 0, + HYBBX_DATE_ISO_SHORT = 1, + HYBBX_DATE_US = 2, + HYBBX_DATE_EU = 3, +} hybbx_date_format_t; + +typedef struct hybbx_time_format { + int clock_12h; + int seconds; + hybbx_date_format_t date_format; +} hybbx_time_format_t; + +void hybbx_time_format_defaults(hybbx_time_format_t *fmt); +const hybbx_time_format_t *hybbx_time_format_get(void); +void hybbx_time_config_apply(const struct hybbx_config *config); + +/** Local wall-clock time for text tokens and stamps. */ +hybbx_result_t hybbx_time_local_now(struct tm *out); + +const char *hybbx_date_format_name(hybbx_date_format_t fmt); + +/** + * Format @p tm as clock time (default @c HH:MM:SS, 12h when configured). + */ +hybbx_result_t hybbx_time_format_time(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt); + +/** + * Format @p tm as calendar date (default @c YYYY/MM/DD). + */ +hybbx_result_t hybbx_time_format_date(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt); + +/** + * Format @p tm as @c yyyymmdd HH:MM per @p fmt (compact log stamp). + * @return HYBBX_OK on success. + */ +hybbx_result_t hybbx_time_format_stamp(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_UTIL_H */ diff --git a/include/hybbx/websocket.h b/include/hybbx/websocket.h new file mode 100644 index 0000000..d14e870 --- /dev/null +++ b/include/hybbx/websocket.h @@ -0,0 +1,53 @@ +#ifndef HYBBX_WEBSOCKET_H +#define HYBBX_WEBSOCKET_H + +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Default WebSocket listen port (behind TLS reverse proxy). */ +#define HYBBX_WEBSOCKET_DEFAULT_PORT 4591u + +/** Default simultaneous WebSocket client connections. */ +#define HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS 10u + +#define HYBBX_WEBSOCKET_BIND_V4_MAX 64 +#define HYBBX_WEBSOCKET_BIND_V6_MAX 64 +#define HYBBX_WEBSOCKET_PATH_MAX 128 + +#define HYBBX_WEBSOCKET_DEFAULT_BIND_V4 "127.0.0.1" +#define HYBBX_WEBSOCKET_DEFAULT_BIND_V6 "::1" +#define HYBBX_WEBSOCKET_DEFAULT_PATH "/hybbx" +#define HYBBX_WEBSOCKET_DEFAULT_CERT_DIR "keys" + +/** Filenames inside @c cert_dir (auto-generated on first start when TLS). */ +#define HYBBX_WS_TLS_CERT_FILENAME "hybbx_ws.crt" +#define HYBBX_WS_TLS_KEY_FILENAME "hybbx_ws.key" + +/** Self-signed WebSocket TLS certificate validity (days). */ +#define HYBBX_WS_TLS_CERT_VALID_DAYS 1825u + +typedef struct hybbx_websocket_config { + char bind_v4[HYBBX_WEBSOCKET_BIND_V4_MAX]; + char bind_v6[HYBBX_WEBSOCKET_BIND_V6_MAX]; + char path[HYBBX_WEBSOCKET_PATH_MAX]; + char cert_dir[HYBBX_PATH_MAX]; + unsigned int port; + unsigned int max_connections; + int ipv4; + int ipv6; +} hybbx_websocket_config_t; + +void hybbx_websocket_config_defaults(hybbx_websocket_config_t *config); + +hybbx_result_t hybbx_websocket_config_parse(const char *config, + hybbx_websocket_config_t *out); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WEBSOCKET_H */ diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..9736302 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,44 @@ +# Link a transport plugin into all HyBBX instance binaries (hybbxd/hybbxsd/hybbxpd). +function(hybbx_link_plugin_instances plugin_target define_name) + foreach(_bin IN ITEMS hybbxd hybbxsd hybbxpd) + if(TARGET ${_bin}) + target_link_libraries(${_bin} PRIVATE ${plugin_target}) + if(NOT "${define_name}" STREQUAL "") + target_compile_definitions(${_bin} PRIVATE ${define_name}) + endif() + endif() + endforeach() +endfunction() + +if(HYBBX_PLUGIN_TELNET) + add_subdirectory(telnet) +endif() + +if(HYBBX_PLUGIN_PACKET_RADIO) + add_subdirectory(packet_radio) +endif() + +if(HYBBX_PLUGIN_ARDOP OR HYBBX_PLUGIN_CRDOP) + add_subdirectory(ardop) +endif() + +if(HYBBX_PLUGIN_CRDOP) + add_subdirectory(crdop) +endif() + +if(HYBBX_PLUGIN_BAYCOM) + add_subdirectory(baycom) +endif() + +if(HYBBX_PLUGIN_MAINS_PROXY) + add_subdirectory(mains_proxy) + target_compile_definitions(hybbx_core PRIVATE HYBBX_HAVE_PLUGIN_MAINS_PROXY) +endif() + +if(HYBBX_PLUGIN_SSH AND HYBBX_HAVE_LIBSSH) + add_subdirectory(ssh) +endif() + +if(HYBBX_PLUGIN_WEBSOCKET) + add_subdirectory(websocket) +endif() diff --git a/plugins/ardop/CMakeLists.txt b/plugins/ardop/CMakeLists.txt new file mode 100644 index 0000000..c39ab91 --- /dev/null +++ b/plugins/ardop/CMakeLists.txt @@ -0,0 +1,57 @@ +add_library(hybbx_ardop_common STATIC + ardop_link.c + ardop_host.c + ardop_crc.c + ardop_config.c +) + +target_include_directories(hybbx_ardop_common + PUBLIC + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_ardop_common + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_ardop_common PUBLIC hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_ardop_common PUBLIC amiga) +endif() +hybbx_apply_hardening(hybbx_ardop_common) + +if(HYBBX_PLUGIN_ARDOP) + add_library(hybbx_plugin_ardop STATIC + ardop.c + ) + + target_include_directories(hybbx_plugin_ardop + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ) + + target_compile_definitions(hybbx_plugin_ardop + PRIVATE HYBBX_PLUGIN_BUILD + ) + + target_link_libraries(hybbx_plugin_ardop PRIVATE hybbx_ardop_common) + hybbx_link_plugin_instances(hybbx_plugin_ardop HYBBX_HAVE_PLUGIN_ARDOP) + hybbx_apply_hardening(hybbx_plugin_ardop) + + if(HYBBX_INSTALL_DEV) + install(TARGETS hybbx_plugin_ardop + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + ) + endif() +endif() + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_ardop_common + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/ardop/ardop.c b/plugins/ardop/ardop.c new file mode 100644 index 0000000..e07a158 --- /dev/null +++ b/plugins/ardop/ardop.c @@ -0,0 +1,49 @@ +/* + * ardop — ARDOP host-client link adapter (external ARDOPC / ardopcf). + * INI: [transport.ardop] or [transport.ardopN]. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/ardop.h" +#include "ardop_link.h" + +static const ardop_link_plugin_t g_ardop_link = { + .name = "ardop", + .kind = HYBBX_TRANSPORT_ARDOP, + .default_link_id = "ardop-link", + .modem_hint = "ARDOPC or ardopcf", + .parse_config = (ardop_link_config_fn)hybbx_ardop_config_parse, + .config_free = (void (*)(void *))hybbx_ardop_config_free, + .config_size = sizeof(hybbx_ardop_config_t), +}; + +static hybbx_result_t ardop_init(hybbx_service_t *service) +{ + return ardop_link_init(service, &g_ardop_link); +} + +static void ardop_shutdown(void) +{ + ardop_link_shutdown(&g_ardop_link); +} + +static hybbx_result_t ardop_start(const char *config) +{ + return ardop_link_start(&g_ardop_link, config); +} + +static hybbx_result_t ardop_stop(void) +{ + return ardop_link_stop(&g_ardop_link); +} + +const hybbx_transport_plugin_t hybbx_plugin_ardop = { + .name = "ardop", + .kind = HYBBX_TRANSPORT_ARDOP, + .version = 1, + .init = ardop_init, + .shutdown = ardop_shutdown, + .start = ardop_start, + .stop = ardop_stop, + .write = NULL, +}; diff --git a/plugins/ardop/ardop_config.c b/plugins/ardop/ardop_config.c new file mode 100644 index 0000000..71b40dc --- /dev/null +++ b/plugins/ardop/ardop_config.c @@ -0,0 +1,186 @@ +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include + +static char *ardop_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_ardop_config_free(hybbx_ardop_config_t *config) +{ + if (config == NULL) { + return; + } + + free(config->ardop_host); + free(config->mycall); + free(config->arq_bandwidth); + free(config->peer_call); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + memset(config, 0, sizeof(*config)); +} + +hybbx_result_t hybbx_ardop_config_parse(const char *config, + hybbx_ardop_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ardop_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_kv(config, "ardop_host", scratch, sizeof(scratch)); + out->ardop_host = ardop_strdup(value != NULL ? value : "127.0.0.1"); + + out->ardop_port = (unsigned)strtoul( + find_kv(config, "ardop_port", scratch, sizeof(scratch)) != NULL ? + scratch : "8515", NULL, 10); + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + out->mycall = ardop_strdup(value != NULL ? value : "NOCALL"); + + value = find_kv(config, "radio_profile", scratch, sizeof(scratch)); + out->radio_profile = hybbx_crdop_profile_parse(value); + + value = find_kv(config, "arq_bandwidth", scratch, sizeof(scratch)); + out->arq_bandwidth = ardop_strdup( + value != NULL ? value : + hybbx_crdop_default_arq_bandwidth(out->radio_profile)); + + value = find_kv(config, "peer_call", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->peer_call = ardop_strdup(value); + } + + value = find_kv(config, "listen", scratch, sizeof(scratch)); + out->listen = value == NULL || str_ieq(value, "yes") || str_ieq(value, "1") || + str_ieq(value, "true"); + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = ardop_strdup(value != NULL ? value : "127.0.0.1"); + + out->circuit_port = (unsigned)strtoul( + find_kv(config, "circuit_port", scratch, sizeof(scratch)) != NULL ? + scratch : "7323", NULL, 10); + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = ardop_strdup(value != NULL ? value : "ardop-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = ardop_strdup(value); + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = ardop_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = strtod(value, NULL); + } + + if (out->radio_profile == HYBBX_CRDOP_PROFILE_CB && + hybbx_crdop_bandwidth_exceeds_cb(out->arq_bandwidth)) { + hybbx_log_warn("[ardop] warning: arq_bandwidth=%s high for CB profile " + "(CRDOP Level 2 experimental)", + out->arq_bandwidth); + } + + if (out->ardop_host == NULL || out->mycall == NULL || + out->arq_bandwidth == NULL || out->circuit_host == NULL || + out->link_id == NULL || out->link_role == NULL) { + hybbx_ardop_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/ardop/ardop_crc.c b/plugins/ardop/ardop_crc.c new file mode 100644 index 0000000..4cf205a --- /dev/null +++ b/plugins/ardop/ardop_crc.c @@ -0,0 +1,69 @@ +#include "ardop_crc.h" + +/* Algorithm from ARDOPC (KN6KB / G8BPQ) — CRC-16-CCITT variant, poly 0x8810. */ + +unsigned int ardop_crc16(const unsigned char *data, unsigned short length) +{ + int int_register = 0xffff; + int i; + int j; + int bit; + const int int_poly = 0x8810; + + if (data == NULL || length == 0) { + return 0xffffu; + } + + for (j = 0; j < (int)length; j++) { + int mask = 0x80; + + for (i = 0; i < 8; i++) { + bit = data[j] & mask; + mask >>= 1; + + if (int_register & 0x8000) { + if (bit) { + int_register = 0xffff & (1 + (int_register << 1)); + } else { + int_register = 0xffff & (int_register << 1); + } + int_register = int_register ^ int_poly; + } else { + if (bit) { + int_register = 0xffff & (1 + (int_register << 1)); + } else { + int_register = 0xffff & (int_register << 1); + } + } + } + } + + return (unsigned int)int_register; +} + +int ardop_crc16_valid(const unsigned char *data, unsigned short length) +{ + unsigned int crc; + + if (data == NULL || length < 3) { + return 0; + } + + crc = ardop_crc16(data, (unsigned short)(length - 2)); + return data[length - 2] == (unsigned char)(crc >> 8) && + data[length - 1] == (unsigned char)(crc & 0xff); +} + +size_t ardop_crc16_append(unsigned char *data, size_t length, size_t cap) +{ + unsigned int crc; + + if (data == NULL || length + 2 > cap) { + return 0; + } + + crc = ardop_crc16(data, (unsigned short)length); + data[length] = (unsigned char)(crc >> 8); + data[length + 1] = (unsigned char)(crc & 0xff); + return length + 2; +} diff --git a/plugins/ardop/ardop_crc.h b/plugins/ardop/ardop_crc.h new file mode 100644 index 0000000..69c2f07 --- /dev/null +++ b/plugins/ardop/ardop_crc.h @@ -0,0 +1,15 @@ +#ifndef ARDOP_CRC_H +#define ARDOP_CRC_H + +#include + +/** ARDOP host/TNC CRC-16 (poly 0x8810, init 0xFFFF) per ARDOPC reference. */ +unsigned int ardop_crc16(const unsigned char *data, unsigned short length); + +/** Non-zero when @p length includes trailing 2-byte CRC that validates. */ +int ardop_crc16_valid(const unsigned char *data, unsigned short length); + +/** Append CRC16 (MSB, LSB) after @p length bytes; returns new length. */ +size_t ardop_crc16_append(unsigned char *data, size_t length, size_t cap); + +#endif /* ARDOP_CRC_H */ diff --git a/plugins/ardop/ardop_host.c b/plugins/ardop/ardop_host.c new file mode 100644 index 0000000..aeac403 --- /dev/null +++ b/plugins/ardop/ardop_host.c @@ -0,0 +1,650 @@ +#include "ardop_host.h" +#include "ardop_crc.h" + +#include "hybbx/socket.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ARDOP_HOST_RX_MAX 8192u +#define ARDOP_HOST_CMD_MAX 256u +#define ARDOP_HOST_FRAME_MAX (HYBBX_ARDOP_DATA_MAX + 64u) + +struct ardop_host { + hybbx_ardop_config_t cfg; + ardop_host_data_fn on_data; + ardop_host_event_fn on_event; + void *userdata; + int ctrl_fd; + int data_fd; + int tnc_ready; + int link_up; + char peer_call[16]; + unsigned char ctrl_rx[ARDOP_HOST_RX_MAX]; + size_t ctrl_len; + unsigned char data_rx[ARDOP_HOST_RX_MAX]; + size_t data_len; + int init_sent; + const char *log_tag; +}; + +static const char *host_log_tag(const ardop_host_t *host) +{ + if (host != NULL && host->log_tag != NULL && host->log_tag[0] != '\0') { + return host->log_tag; + } + return "ardop"; +} + +static const char *host_modem_name(const ardop_host_t *host) +{ + if (host != NULL && host->log_tag != NULL && + strcmp(host->log_tag, "crdop") == 0) { + return "CRDOPC"; + } + return "ARDOPC"; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void ardop_host_fire_event(ardop_host_t *host, const char *event, + const char *detail) +{ + if (host != NULL && host->on_event != NULL) { + host->on_event(event, detail, host->userdata); + } +} + +static int ardop_host_set_socket_opts(int fd) +{ + int on = 1; + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + return 0; +} + +static int ardop_host_tcp_connect(const char *host, unsigned port) +{ + struct sockaddr_in addr; + int fd; + int rc; + + if (host == NULL || host[0] == '\0' || port == 0) { + return -1; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (ardop_host_set_socket_opts(fd) != 0) { + close(fd); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc != 0) { + close(fd); + return -1; + } + + return fd; +} + +static hybbx_result_t ardop_host_write_all(int fd, const unsigned char *buf, + size_t len) +{ + size_t off = 0; + + if (fd < 0 || buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + while (off < len) { + ssize_t n = send(fd, buf + off, len - off, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)n; + } + + return HYBBX_OK; +} + +static hybbx_result_t ardop_host_send_cmd_frame(ardop_host_t *host, + const char *cmd) +{ + unsigned char frame[ARDOP_HOST_CMD_MAX + 4]; + size_t len; + + if (host == NULL || cmd == NULL || host->ctrl_fd < 0) { + return HYBBX_ERR_INVALID; + } + + len = strlen(cmd); + if (len + 3 > sizeof(frame)) { + return HYBBX_ERR_INVALID; + } + + memcpy(frame, cmd, len); + frame[len++] = '\r'; + len = ardop_crc16_append(frame, len, sizeof(frame)); + if (len == 0) { + return HYBBX_ERR_IO; + } + + return ardop_host_write_all(host->ctrl_fd, frame, len); +} + +static hybbx_result_t ardop_host_send_rdy(ardop_host_t *host) +{ + return ardop_host_send_cmd_frame(host, "RDY"); +} + +static void ardop_host_strip_prefix(char *line) +{ + if (line == NULL) { + return; + } + + if (strncmp(line, "c:", 2) == 0 || strncmp(line, "C:", 2) == 0) { + memmove(line, line + 2, strlen(line + 2) + 1); + } +} + +static void ardop_host_handle_ctrl_line(ardop_host_t *host, char *line) +{ + char *sp; + + if (host == NULL || line == NULL) { + return; + } + + ardop_host_strip_prefix(line); + + if (str_ieq(line, "RDY")) { + host->tnc_ready = 1; + return; + } + + if (strncmp(line, "VERSION", 7) == 0) { + hybbx_log_debug("[%s] modem %s", host_log_tag(host), line); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "CONNECTED", 9) == 0) { + char *tok; + + sp = line + 9; + while (*sp == ' ') { + sp++; + } + hybbx_strlcpy(host->peer_call, sp, sizeof(host->peer_call)); + tok = strchr(host->peer_call, ' '); + if (tok != NULL) { + *tok = '\0'; + } + host->link_up = 1; + ardop_host_fire_event(host, "connected", host->peer_call); + (void)ardop_host_send_rdy(host); + return; + } + + if (str_ieq(line, "DISCONNECTED")) { + host->link_up = 0; + host->peer_call[0] = '\0'; + ardop_host_fire_event(host, "disconnected", NULL); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "FAULT", 5) == 0 || + strncmp(line, "CRCFAULT", 8) == 0) { + hybbx_log_warn("[%s] TNC fault: %s", host_log_tag(host), line); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "BUSY", 4) == 0 || + strncmp(line, "NEWSTATE", 8) == 0 || + strncmp(line, "BUFFER", 6) == 0 || + strncmp(line, "STATUS", 6) == 0 || + strncmp(line, "PENDING", 7) == 0 || + strncmp(line, "TARGET", 6) == 0) { + (void)ardop_host_send_rdy(host); + return; + } + + (void)ardop_host_send_rdy(host); +} + +static void ardop_host_process_ctrl_rx(ardop_host_t *host) +{ + unsigned char *cr; + unsigned char frame[ARDOP_HOST_CMD_MAX + 8]; + size_t msg_len; + char line[ARDOP_HOST_CMD_MAX]; + + if (host == NULL) { + return; + } + + for (;;) { + cr = memchr(host->ctrl_rx, '\r', host->ctrl_len); + if (cr == NULL) { + return; + } + + msg_len = (size_t)(cr - host->ctrl_rx) + 1u + 2u; + if (host->ctrl_len < msg_len) { + return; + } + + if (msg_len > sizeof(frame)) { + host->ctrl_len = 0; + return; + } + + memcpy(frame, host->ctrl_rx, msg_len); + if (!ardop_crc16_valid(frame, (unsigned short)msg_len)) { + hybbx_log_warn("[%s] control CRC fault", host_log_tag(host)); + host->ctrl_len = 0; + return; + } + + frame[msg_len - 3] = '\0'; + hybbx_strlcpy(line, (const char *)frame, sizeof(line)); + ardop_host_handle_ctrl_line(host, line); + + memmove(host->ctrl_rx, host->ctrl_rx + msg_len, host->ctrl_len - msg_len); + host->ctrl_len -= msg_len; + } +} + +static void ardop_host_process_data_rx(ardop_host_t *host) +{ + size_t off = 0; + + if (host == NULL) { + return; + } + + while (host->data_len >= off + 6u) { + const unsigned char *buf = host->data_rx + off; + size_t remain = host->data_len - off; + size_t payload_len; + size_t frame_len; + const uint8_t *payload; + const char *tag; + + if (buf[0] != 'd' && buf[0] != 'D') { + off++; + continue; + } + if (buf[1] != ':') { + off++; + continue; + } + + payload_len = ((size_t)buf[2] << 8) | (size_t)buf[3]; + if (payload_len < 3 || payload_len > HYBBX_ARDOP_DATA_MAX + 8u) { + off++; + continue; + } + + frame_len = 2u + 2u + payload_len + 2u; + if (remain < frame_len) { + break; + } + + if (!ardop_crc16_valid(buf, (unsigned short)frame_len)) { + off++; + continue; + } + + tag = (const char *)(buf + 4); + payload = buf + 7; + payload_len -= 3u; + + if (strncmp(tag, "ARQ", 3) == 0 && host->on_data != NULL && + payload_len > 0) { + host->on_data(payload, payload_len, host->userdata); + } + + (void)ardop_host_send_cmd_frame(host, "RDY"); + off += frame_len; + } + + if (off > 0) { + memmove(host->data_rx, host->data_rx + off, host->data_len - off); + host->data_len -= off; + } +} + +static hybbx_result_t ardop_host_send_init(ardop_host_t *host) +{ + char cmd[96]; + hybbx_result_t rc; + + if (host == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = ardop_host_send_cmd_frame(host, "INITIALIZE"); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(cmd, sizeof(cmd), "MYCALL %s", + host->cfg.mycall != NULL ? host->cfg.mycall : "NOCALL"); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + + rc = ardop_host_send_cmd_frame(host, "PROTOCOLMODE ARQ"); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(cmd, sizeof(cmd), "ARQBW %s", + host->cfg.arq_bandwidth != NULL ? host->cfg.arq_bandwidth : + "500MAX"); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + + rc = ardop_host_send_cmd_frame(host, + host->cfg.listen ? "LISTEN TRUE" : + "LISTEN FALSE"); + if (rc != HYBBX_OK) { + return rc; + } + + if (host->cfg.peer_call != NULL && host->cfg.peer_call[0] != '\0') { + snprintf(cmd, sizeof(cmd), "ARQCALL %s 5", host->cfg.peer_call); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + host->init_sent = 1; + hybbx_log_info("[%s] host init sent (external %s at %s:%u)", + host_log_tag(host), host_modem_name(host), + host->cfg.ardop_host != NULL ? host->cfg.ardop_host : "?", + host->cfg.ardop_port); + return HYBBX_OK; +} + +static void ardop_host_read_fd(int fd, unsigned char *buf, size_t *len, + size_t cap) +{ + ssize_t n; + + if (fd < 0 || buf == NULL || len == NULL || *len >= cap) { + return; + } + + n = recv(fd, buf + *len, cap - *len, 0); + if (n > 0) { + *len += (size_t)n; + } else if (n == 0) { + close(fd); + } +} + +ardop_host_t *ardop_host_create(const hybbx_ardop_config_t *cfg, + ardop_host_data_fn on_data, + ardop_host_event_fn on_event, + void *userdata, + const char *log_tag) +{ + ardop_host_t *host; + + if (cfg == NULL) { + return NULL; + } + + host = calloc(1, sizeof(*host)); + if (host == NULL) { + return NULL; + } + + host->cfg = *cfg; + host->on_data = on_data; + host->on_event = on_event; + host->userdata = userdata; + host->ctrl_fd = -1; + host->data_fd = -1; + host->log_tag = log_tag; + return host; +} + +void ardop_host_destroy(ardop_host_t *host) +{ + if (host == NULL) { + return; + } + + ardop_host_disconnect(host); + free(host); +} + +hybbx_result_t ardop_host_connect(ardop_host_t *host) +{ + const char *h; + unsigned port; + + if (host == NULL) { + return HYBBX_ERR_INVALID; + } + + ardop_host_disconnect(host); + + h = host->cfg.ardop_host; + if (h == NULL || h[0] == '\0') { + h = "127.0.0.1"; + } + port = host->cfg.ardop_port; + if (port == 0) { + port = HYBBX_ARDOP_DEFAULT_PORT; + } + + host->ctrl_fd = ardop_host_tcp_connect(h, port); + if (host->ctrl_fd < 0) { + hybbx_log_warn("[%s] cannot connect to %s control %s:%u", + host_log_tag(host), host_modem_name(host), h, port); + return HYBBX_ERR_IO; + } + + host->data_fd = ardop_host_tcp_connect(h, port + 1u); + if (host->data_fd < 0) { + hybbx_log_warn("[%s] cannot connect to %s data %s:%u", + host_log_tag(host), host_modem_name(host), h, port + 1u); + ardop_host_disconnect(host); + return HYBBX_ERR_IO; + } + + host->tnc_ready = 0; + host->init_sent = 0; + hybbx_log_info("[%s] connected to external %s %s:%u (data %u)", + host_log_tag(host), host_modem_name(host), h, port, port + 1u); + + (void)ardop_host_send_init(host); + { + unsigned spin; + + for (spin = 0; spin < 8u; spin++) { + ardop_host_poll(host); + if (host->tnc_ready) { + break; + } + } + } + return HYBBX_OK; +} + +void ardop_host_disconnect(ardop_host_t *host) +{ + if (host == NULL) { + return; + } + + if (host->ctrl_fd >= 0) { + close(host->ctrl_fd); + host->ctrl_fd = -1; + } + if (host->data_fd >= 0) { + close(host->data_fd); + host->data_fd = -1; + } + + host->ctrl_len = 0; + host->data_len = 0; + host->tnc_ready = 0; + host->link_up = 0; + host->init_sent = 0; + host->peer_call[0] = '\0'; +} + +void ardop_host_poll(ardop_host_t *host) +{ + struct pollfd pfds[2]; + int count = 0; + int pr; + + if (host == NULL || host->ctrl_fd < 0) { + return; + } + + pfds[0].fd = host->ctrl_fd; + pfds[0].events = POLLIN; + count = 1; + + if (host->data_fd >= 0) { + pfds[1].fd = host->data_fd; + pfds[1].events = POLLIN; + count = 2; + } + + pr = poll(pfds, (nfds_t)count, 0); + if (pr <= 0) { + if (host->tnc_ready && !host->init_sent) { + (void)ardop_host_send_init(host); + } + return; + } + + if ((pfds[0].revents & POLLIN) != 0) { + ardop_host_read_fd(host->ctrl_fd, host->ctrl_rx, &host->ctrl_len, + ARDOP_HOST_RX_MAX); + ardop_host_process_ctrl_rx(host); + } + + if (count > 1 && (pfds[1].revents & POLLIN) != 0) { + ardop_host_read_fd(host->data_fd, host->data_rx, &host->data_len, + ARDOP_HOST_RX_MAX); + ardop_host_process_data_rx(host); + } + + if (host->tnc_ready && !host->init_sent) { + (void)ardop_host_send_init(host); + } +} + +int ardop_host_link_up(const ardop_host_t *host) +{ + return host != NULL && host->link_up; +} + +int ardop_host_modem_connected(const ardop_host_t *host) +{ + return host != NULL && host->ctrl_fd >= 0; +} + +hybbx_result_t ardop_host_send_data(ardop_host_t *host, + const uint8_t *data, size_t len) +{ + unsigned char frame[ARDOP_HOST_FRAME_MAX]; + size_t frame_len; + + if (host == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (!host->link_up || host->data_fd < 0) { + return HYBBX_ERR_BUSY; + } + + if (len > HYBBX_ARDOP_DATA_MAX) { + return HYBBX_ERR_INVALID; + } + + if (len + 8 > sizeof(frame)) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 'D'; + frame[1] = ':'; + frame[2] = (unsigned char)((len >> 8) & 0xff); + frame[3] = (unsigned char)(len & 0xff); + memcpy(frame + 4, data, len); + + frame_len = 4u + len; + frame_len = ardop_crc16_append(frame, frame_len, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return ardop_host_write_all(host->data_fd, frame, frame_len); +} diff --git a/plugins/ardop/ardop_host.h b/plugins/ardop/ardop_host.h new file mode 100644 index 0000000..4aee53d --- /dev/null +++ b/plugins/ardop/ardop_host.h @@ -0,0 +1,47 @@ +#ifndef ARDOP_HOST_H +#define ARDOP_HOST_H + +/** + * Minimal ARDOP TNC Host Interface client (TCP control + data ports). + * Implements the subset HyBBX needs — see docs/MANUAL.md § ARDOP. + */ + +#include "hybbx/types.h" +#include "hybbx/ardop.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ardop_host ardop_host_t; + +typedef void (*ardop_host_data_fn)(const uint8_t *data, size_t len, void *ctx); +typedef void (*ardop_host_event_fn)(const char *event, const char *detail, + void *ctx); + +ardop_host_t *ardop_host_create(const hybbx_ardop_config_t *cfg, + ardop_host_data_fn on_data, + ardop_host_event_fn on_event, + void *userdata, + const char *log_tag); +void ardop_host_destroy(ardop_host_t *host); + +hybbx_result_t ardop_host_connect(ardop_host_t *host); +void ardop_host_disconnect(ardop_host_t *host); + +/** Drive I/O and state machine; call periodically from plugin poll thread. */ +void ardop_host_poll(ardop_host_t *host); + +int ardop_host_link_up(const ardop_host_t *host); + +/** Control TCP session to external ARDOPC is open. */ +int ardop_host_modem_connected(const ardop_host_t *host); + +hybbx_result_t ardop_host_send_data(ardop_host_t *host, + const uint8_t *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* ARDOP_HOST_H */ diff --git a/plugins/ardop/ardop_link.c b/plugins/ardop/ardop_link.c new file mode 100644 index 0000000..e0500b9 --- /dev/null +++ b/plugins/ardop/ardop_link.c @@ -0,0 +1,446 @@ +#include "ardop_link.h" +#include "ardop_host.h" + +#include "hybbx/ardop.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/crdop.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +static const ardop_link_plugin_t *g_plugin; +static hybbx_ardop_config_t g_active_config; +static ardop_host_t *g_host; +static int g_circuit_fd = -1; +static hybbx_circuit_decoder_t g_circuit_dec; +static pthread_t g_poll_thread; +static volatile int g_running; +static int g_poll_thread_started; +static volatile int g_circuit_flow_paused; +static volatile int g_circuit_flow_cancel; +static unsigned g_modem_reconnect_backoff_ms; + +static const char *link_tag(void) +{ + return g_plugin != NULL && g_plugin->name != NULL ? g_plugin->name : "ardop"; +} + +static int circuit_uplink_allowed(void) +{ + return !g_circuit_flow_paused && !g_circuit_flow_cancel; +} + +static hybbx_result_t circuit_uplink_terminal(const uint8_t *payload, size_t len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + if (g_circuit_fd < 0 || len == 0) { + return HYBBX_ERR_IO; + } + + if (!circuit_uplink_allowed()) { + return HYBBX_OK; + } + + hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_RX, + payload, len, hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(g_circuit_fd, hbx, hbx_len); +} + +static void on_modem_data(const uint8_t *data, size_t len, void *userdata) +{ + (void)userdata; + + if (len == 0 || g_circuit_fd < 0 || !circuit_uplink_allowed()) { + return; + } + + (void)circuit_uplink_terminal(data, len); +} + +static void on_modem_event(const char *event, const char *detail, void *userdata) +{ + (void)userdata; + + if (event == NULL) { + return; + } + + if (strcmp(event, "connected") == 0) { + if (detail != NULL && detail[0] != '\0') { + hybbx_log_stats("[%s] RF link up (%s)", link_tag(), detail); + } else { + hybbx_log_stats("[%s] RF link up", link_tag()); + } + g_modem_reconnect_backoff_ms = 1000u; + return; + } + + if (strcmp(event, "disconnected") == 0) { + hybbx_log_stats("[%s] RF link down", link_tag()); + } +} + +static void on_circuit_flow_ctrl(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + (void)proto; + (void)flags; + (void)userdata; + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + g_circuit_flow_paused = 1; + hybbx_log_stats("[%s] circuit flow pause (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + g_circuit_flow_paused = 0; + hybbx_circuit_decoder_init(&g_circuit_dec); + hybbx_log_stats("[%s] circuit flow break (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + g_circuit_flow_cancel = 1; + hybbx_log_warn("[%s] circuit flow cancel (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + g_circuit_flow_paused = 0; + hybbx_log_stats("[%s] circuit flow resume (%s)", link_tag(), reason); + break; + default: + break; + } +} + +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + (void)flags; + (void)userdata; + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + on_circuit_flow_ctrl(proto, flags, payload, len, userdata); + return; + } + + if (g_host == NULL || len == 0 || g_circuit_flow_paused) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_TERMINAL) { + return; + } + + if (!ardop_host_link_up(g_host)) { + return; + } + + (void)ardop_host_send_data(g_host, payload, len); +} + +static void link_poll_sleep_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(ms / 1000u); + tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); +#endif +} + +static hybbx_result_t link_connect_circuit(void); + +static void link_maybe_reconnect_modem(void) +{ + static unsigned ticks; + + if (g_host == NULL) { + return; + } + + if (ardop_host_modem_connected(g_host)) { + ticks = 0; + return; + } + + ticks++; + if (ticks < (g_modem_reconnect_backoff_ms / 20u)) { + return; + } + + ticks = 0; + if (ardop_host_connect(g_host) != HYBBX_OK) { + if (g_modem_reconnect_backoff_ms < 30000u) { + g_modem_reconnect_backoff_ms += 1000u; + } + } +} + +static void *link_poll_thread(void *arg) +{ + uint8_t buf[512]; + + (void)arg; + + while (g_running) { + if (g_host != NULL) { + ardop_host_poll(g_host); + link_maybe_reconnect_modem(); + } + + if (g_circuit_fd >= 0) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(g_circuit_fd, buf, + sizeof(buf), &read_len); + if (rc == HYBBX_OK && read_len > 0) { + hybbx_circuit_decoder_feed(&g_circuit_dec, buf, read_len, + on_circuit_downlink, NULL); + } + } else { + static unsigned circuit_ticks; + + circuit_ticks++; + if (circuit_ticks >= 50u) { + circuit_ticks = 0; + (void)link_connect_circuit(); + } + } + + if (g_circuit_flow_cancel) { + g_circuit_flow_cancel = 0; + g_circuit_flow_paused = 0; + if (g_circuit_fd >= 0) { + close(g_circuit_fd); + g_circuit_fd = -1; + } + (void)link_connect_circuit(); + } + + link_poll_sleep_ms(20); + } + + return NULL; +} + +static hybbx_result_t link_connect_circuit(void) +{ + const char *host = g_active_config.circuit_host; + unsigned port = g_active_config.circuit_port; + const char *link_id = g_active_config.link_id; + unsigned attempt; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + if (link_id == NULL || link_id[0] == '\0') { + link_id = g_plugin != NULL && g_plugin->default_link_id != NULL ? + g_plugin->default_link_id : "ardop-link"; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &g_circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&g_circuit_dec); + if (g_active_config.link_password != NULL && + g_active_config.link_password[0] != '\0') { + const char *link_role = g_active_config.link_role; + hybbx_circuit_link_qos_t qos; + char freq_buf[16]; + const char *freq_ptr = NULL; + + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + memset(&qos, 0, sizeof(qos)); + qos.bandwidth = "low"; + qos.duplex = 1; + if (g_active_config.frequency_mhz > 0.0) { + snprintf(freq_buf, sizeof(freq_buf), "%.3f", + g_active_config.frequency_mhz); + freq_ptr = freq_buf; + } + qos.frequency_mhz = freq_ptr; + + rc = hybbx_circuit_link_authenticate_ex( + g_circuit_fd, g_active_config.link_password, + link_role, link_id, &qos); + if (rc != HYBBX_OK) { + close(g_circuit_fd); + g_circuit_fd = -1; + link_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[%s] linked to internal circuit %s:%u (HBX)", + link_tag(), host, port); + return HYBBX_OK; + } + link_poll_sleep_ms(100); + } + + hybbx_log_warn("[%s] could not connect to internal circuit %s:%u", + link_tag(), host, port); + return HYBBX_ERR_IO; +} + +static void link_shutdown(void) +{ + g_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + if (g_circuit_fd >= 0) { + close(g_circuit_fd); + g_circuit_fd = -1; + } + + if (g_host != NULL) { + ardop_host_destroy(g_host); + g_host = NULL; + } + + if (g_plugin != NULL && g_plugin->config_free != NULL) { + g_plugin->config_free(&g_active_config); + } +} + +hybbx_result_t ardop_link_init(struct hybbx_service *service, + const ardop_link_plugin_t *plugin) +{ + (void)service; + g_plugin = plugin; + g_host = NULL; + g_circuit_fd = -1; + g_running = 0; + g_circuit_flow_paused = 0; + g_circuit_flow_cancel = 0; + g_modem_reconnect_backoff_ms = 1000u; + return HYBBX_OK; +} + +void ardop_link_shutdown(const ardop_link_plugin_t *plugin) +{ + (void)plugin; + link_shutdown(); +} + +hybbx_result_t ardop_link_start(const ardop_link_plugin_t *plugin, + const char *config) +{ + hybbx_result_t rc; + const char *modem_hint; + + if (plugin == NULL || plugin->parse_config == NULL) { + return HYBBX_ERR_INVALID; + } + + g_plugin = plugin; + link_shutdown(); + memset(&g_active_config, 0, sizeof(g_active_config)); + g_poll_thread = (pthread_t)0; + + rc = plugin->parse_config(config, &g_active_config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] invalid configuration", link_tag()); + return rc; + } + + hybbx_log_info("[%s] external modem %s:%u mycall=%s arq=%s profile=%s circuit=%s:%u " + "link_id=%s", + link_tag(), + g_active_config.ardop_host, + g_active_config.ardop_port, + g_active_config.mycall, + g_active_config.arq_bandwidth, + hybbx_crdop_profile_name(g_active_config.radio_profile), + g_active_config.circuit_host, + g_active_config.circuit_port, + g_active_config.link_id != NULL ? g_active_config.link_id : "?"); + + if (g_active_config.radio_profile == HYBBX_CRDOP_PROFILE_CB) { + hybbx_log_info("[%s] CB half-duplex profile (experimental Level 2)", + link_tag()); + } + if (g_active_config.frequency_mhz > 0.0) { + hybbx_log_info("[%s] frequency_mhz=%.3f", link_tag(), + g_active_config.frequency_mhz); + } + + modem_hint = plugin->modem_hint != NULL ? plugin->modem_hint : "ARDOPC"; + hybbx_log_info("[%s] operator must run external %s separately (e.g. ardopc TCPIP %u)", + link_tag(), modem_hint, g_active_config.ardop_port); + + g_host = ardop_host_create(&g_active_config, on_modem_data, on_modem_event, + NULL, link_tag()); + if (g_host == NULL) { + link_shutdown(); + return HYBBX_ERR_NOMEM; + } + + rc = ardop_host_connect(g_host); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] external modem not reachable yet — will retry in poll loop", + link_tag()); + } + + rc = link_connect_circuit(); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] circuit hub not reachable — HBX uplink deferred", + link_tag()); + } + + g_running = 1; + if (pthread_create(&g_poll_thread, NULL, link_poll_thread, NULL) != 0) { + hybbx_log_warn("[%s] poll thread failed", link_tag()); + link_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + return HYBBX_OK; +} + +hybbx_result_t ardop_link_stop(const ardop_link_plugin_t *plugin) +{ + (void)plugin; + hybbx_log_info("[%s] stop", link_tag()); + link_shutdown(); + return HYBBX_OK; +} diff --git a/plugins/ardop/ardop_link.h b/plugins/ardop/ardop_link.h new file mode 100644 index 0000000..0f10dba --- /dev/null +++ b/plugins/ardop/ardop_link.h @@ -0,0 +1,37 @@ +#ifndef ARDOP_LINK_H +#define ARDOP_LINK_H + +#include "hybbx/plugin.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; + +typedef hybbx_result_t (*ardop_link_config_fn)(const char *config, + void *config_out); + +typedef struct ardop_link_plugin { + const char *name; + hybbx_transport_kind_t kind; + const char *default_link_id; + const char *modem_hint; + ardop_link_config_fn parse_config; + void (*config_free)(void *config_out); + size_t config_size; +} ardop_link_plugin_t; + +hybbx_result_t ardop_link_init(struct hybbx_service *service, + const ardop_link_plugin_t *plugin); +void ardop_link_shutdown(const ardop_link_plugin_t *plugin); +hybbx_result_t ardop_link_start(const ardop_link_plugin_t *plugin, + const char *config); +hybbx_result_t ardop_link_stop(const ardop_link_plugin_t *plugin); + +#ifdef __cplusplus +} +#endif + +#endif /* ARDOP_LINK_H */ diff --git a/plugins/baycom/CMakeLists.txt b/plugins/baycom/CMakeLists.txt new file mode 100644 index 0000000..8a2dc2d --- /dev/null +++ b/plugins/baycom/CMakeLists.txt @@ -0,0 +1,36 @@ +add_library(hybbx_plugin_baycom STATIC + baycom.c + baycom_config.c + baycom_modem.c + baycom_kernel.c + baycom_kiss.c + ../packet_radio/ax25.c + ../packet_radio/kiss.c + ../packet_radio/serial_port.c +) + +target_include_directories(hybbx_plugin_baycom + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/plugins/packet_radio + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_baycom + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_plugin_baycom PRIVATE hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_plugin_baycom PRIVATE amiga) +endif() +hybbx_link_plugin_instances(hybbx_plugin_baycom HYBBX_HAVE_PLUGIN_BAYCOM) +hybbx_apply_hardening(hybbx_plugin_baycom) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_baycom + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/baycom/baycom.c b/plugins/baycom/baycom.c new file mode 100644 index 0000000..e2fb5c9 --- /dev/null +++ b/plugins/baycom/baycom.c @@ -0,0 +1,623 @@ +/* + * baycom — BayCom PR-Stack link adapter (Linux kernel SER12/PAR96/EPP or KISS serial). + * INI: [transport.baycom] or [transport.baycomN]. See docs/BAYCOM.md. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/baycom.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/limits.h" +#include "hybbx/max25.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "hybbx/ax25.h" + +#include "baycom_modem.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +typedef struct baycom_instance { + hybbx_baycom_config_t config; + baycom_modem_t *modem; + int circuit_fd; + hybbx_circuit_decoder_t circuit_dec; + volatile int flow_paused; + volatile int flow_cancel; + unsigned circuit_reconnect_polls; + unsigned index; +} baycom_instance_t; + +static hybbx_service_t *g_service; + +static baycom_instance_t g_instances[HYBBX_BAYCOM_MAX_INSTANCES]; +static unsigned g_instance_count; +static pthread_t g_poll_thread; +static volatile int g_running; +static int g_poll_thread_started; + +static void baycom_poll_sleep_ms(unsigned ms); +static hybbx_result_t instance_connect_circuit(baycom_instance_t *inst); +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata); + +static int instance_circuit_uplink_allowed(const baycom_instance_t *inst) +{ + return inst != NULL && !inst->flow_paused && !inst->flow_cancel; +} + +static hybbx_result_t instance_circuit_uplink(baycom_instance_t *inst, + const uint8_t *frame, + size_t frame_len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + if (inst == NULL || inst->circuit_fd < 0 || frame_len == 0) { + return HYBBX_ERR_IO; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return HYBBX_OK; + } + + hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, HYBBX_CIRCUIT_FLAG_RX, + hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); +} + +static void instance_format_call(const hybbx_ax25_address_t *addr, + char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return; + } + + out[0] = '\0'; + if (addr == NULL || addr->call[0] == '\0') { + return; + } + + if (addr->ssid > 0u) { + unsigned ssid = addr->ssid; + + if (ssid > HYBBX_AX25_SSID_MAX) { + ssid = HYBBX_AX25_SSID_MAX; + } + snprintf(out, out_len, "%s-%u", addr->call, ssid); + } else { + hybbx_strlcpy(out, addr->call, out_len); + } +} + +static void instance_note_rf_activity(const baycom_instance_t *inst) +{ + hybbx_circuit_hub_t *hub; + const char *link_id; + + if (inst == NULL || g_service == NULL) { + return; + } + + hub = hybbx_service_circuit_hub(g_service); + link_id = inst->config.link_id; + if (hub != NULL && link_id != NULL && link_id[0] != '\0') { + hybbx_circuit_hub_note_rf_activity(hub, link_id); + } +} + +static void instance_log_rf_tx(const baycom_instance_t *inst, + hybbx_result_t rc, + const hybbx_ax25_path_t *path, + size_t payload_len) +{ + char src[HYBBX_AX25_CALL_MAX + 8]; + char dst[HYBBX_AX25_CALL_MAX + 8]; + + if (inst == NULL) { + return; + } + + instance_format_call(path != NULL ? &path->source : NULL, src, sizeof(src)); + instance_format_call(path != NULL ? &path->dest : NULL, dst, sizeof(dst)); + + if (rc == HYBBX_OK) { + hybbx_log_stats("[baycom%u] RF TX %s>%s (%zu bytes)", + inst->index + 1, src, dst, payload_len); + } else { + hybbx_log_warn("[baycom%u] RF TX failed %s>%s (%zu bytes, rc=%d)", + inst->index + 1, src, dst, payload_len, (int)rc); + } +} + +static void instance_circuit_disconnect(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + hybbx_circuit_decoder_init(&inst->circuit_dec); + inst->circuit_reconnect_polls = 0; +} + +static void instance_circuit_reconnect(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + instance_circuit_disconnect(inst); + baycom_poll_sleep_ms(250); + if (instance_connect_circuit(inst) == HYBBX_OK) { + hybbx_log_info("[baycom%u] circuit reconnected", inst->index + 1); + } +} + +static void instance_drain_circuit(baycom_instance_t *inst) +{ + uint8_t buf[512]; + + if (inst == NULL || inst->circuit_fd < 0) { + return; + } + + for (;;) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(inst->circuit_fd, buf, + sizeof(buf), &read_len); + + if (rc == HYBBX_ERR_IO) { + hybbx_log_warn("[baycom%u] circuit disconnected — reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + return; + } + if (rc != HYBBX_OK || read_len == 0) { + break; + } + + hybbx_circuit_decoder_feed(&inst->circuit_dec, buf, read_len, + on_circuit_downlink, inst); + } +} + +static void on_modem_ax25_frame(const uint8_t *frame, size_t len, void *userdata) +{ + baycom_instance_t *inst = (baycom_instance_t *)userdata; + + if (len == 0 || inst == NULL) { + return; + } + + (void)instance_circuit_uplink(inst, frame, len); +} + +static void baycom_poll_sleep_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(ms / 1000u); + tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); +#endif +} + +static hybbx_result_t instance_connect_circuit(baycom_instance_t *inst) +{ + const char *host; + unsigned port; + unsigned attempt; + + if (inst == NULL) { + return HYBBX_ERR_INVALID; + } + + host = inst->config.circuit_host; + port = inst->config.circuit_port; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &inst->circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&inst->circuit_dec); + if (inst->config.link_password != NULL && + inst->config.link_password[0] != '\0') { + const char *link_id = inst->config.link_id; + const char *link_role = inst->config.link_role; + hybbx_circuit_link_qos_t qos; + + if (link_id == NULL || link_id[0] == '\0') { + link_id = "baycom-link"; + } + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + memset(&qos, 0, sizeof(qos)); + qos.baud = inst->config.radio_baud; + qos.duplex = inst->config.full_duplex ? 2 : 1; + qos.bandwidth = "low"; + qos.frequency_mhz = inst->config.frequency_mhz; + + rc = hybbx_circuit_link_authenticate_ex( + inst->circuit_fd, inst->config.link_password, + link_role, link_id, &qos); + if (rc != HYBBX_OK) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + baycom_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[baycom%u] linked to internal circuit %s:%u (HBX)", + inst->index + 1, host, port); + return HYBBX_OK; + } + baycom_poll_sleep_ms(100); + } + + hybbx_log_warn("[baycom%u] could not connect to internal circuit %s:%u", + inst->index + 1, host, port); + return HYBBX_ERR_IO; +} + +static void instance_on_circuit_flow_ctrl(baycom_instance_t *inst, + const uint8_t *payload, size_t len) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + if (inst == NULL) { + return; + } + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + inst->flow_paused = 1; + hybbx_log_stats("[baycom%u] circuit flow pause (%s)", inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + inst->flow_paused = 0; + hybbx_circuit_decoder_init(&inst->circuit_dec); + hybbx_log_stats("[baycom%u] circuit flow break (%s)", inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + inst->flow_cancel = 1; + hybbx_log_warn("[baycom%u] circuit flow cancel (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + inst->flow_paused = 0; + hybbx_log_stats("[baycom%u] circuit flow resume (%s)", inst->index + 1, reason); + break; + default: + break; + } +} + +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + baycom_instance_t *inst = (baycom_instance_t *)userdata; + + (void)flags; + + if (inst == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + instance_on_circuit_flow_ctrl(inst, payload, len); + return; + } + + if (inst->modem == NULL || len == 0 || inst->flow_paused) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_AX25) { + hybbx_result_t rc = baycom_modem_send_frame(inst->modem, payload, len); + hybbx_ax25_path_t path; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, sizeof(ui)); + + if (rc == HYBBX_OK) { + instance_note_rf_activity(inst); + if (ui_len > 0) { + instance_log_rf_tx(inst, rc, &path, ui_len); + } + } else { + hybbx_log_warn("[baycom%u] RF TX frame failed (rc=%d, %zu bytes)", + inst->index + 1, (int)rc, len); + } + } +} + +static void instance_shutdown(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->modem != NULL) { + baycom_modem_close(inst->modem); + inst->modem = NULL; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + + hybbx_baycom_config_free(&inst->config); + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; +} + +static void *baycom_poll_thread(void *arg) +{ + unsigned i; + + (void)arg; + + while (g_running) { + for (i = 0; i < g_instance_count; i++) { + baycom_instance_t *inst = &g_instances[i]; + + if (inst->modem != NULL) { + (void)baycom_modem_poll(inst->modem); + } + + instance_drain_circuit(inst); + + if (inst->circuit_fd < 0) { + inst->circuit_reconnect_polls++; + if (inst->circuit_reconnect_polls >= 50u) { + inst->circuit_reconnect_polls = 0; + instance_circuit_reconnect(inst); + } + } else { + inst->circuit_reconnect_polls = 0; + } + + if (inst->flow_cancel) { + inst->flow_cancel = 0; + inst->flow_paused = 0; + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + (void)instance_connect_circuit(inst); + } + } + + baycom_poll_sleep_ms(20); + } + + return NULL; +} + +static hybbx_result_t instance_start(baycom_instance_t *inst, + const char *config, + unsigned index) +{ + hybbx_result_t rc; + + if (inst == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; + inst->index = index; + + rc = hybbx_baycom_config_parse(config, &inst->config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom%u] invalid configuration", index + 1); + instance_shutdown(inst); + return rc; + } + + { + char msg[512]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + "[baycom%u] backend=%s mode=%s", + index + 1, + hybbx_baycom_backend_name(inst->config.backend), + hybbx_baycom_modem_mode_name(inst->config.mode)); + + if (inst->config.backend == HYBBX_BAYCOM_BACKEND_KERNEL) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " interface=%s module=%s iobase=0x%x irq=%u radio_baud=%u", + inst->config.interface != NULL ? inst->config.interface : "?", + inst->config.kernel_module != NULL ? inst->config.kernel_module + : "?", + inst->config.iobase, inst->config.irq, inst->config.radio_baud); + } else { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " device=%s serial_baud=%u", + inst->config.device != NULL ? inst->config.device : "?", + inst->config.serial_baud); + } + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " circuit=%s:%u (HBX over internal TCP)", + inst->config.circuit_host, inst->config.circuit_port); + hybbx_log_info("%s", msg); + } + + if (inst->config.frequency_mhz != NULL && + inst->config.frequency_mhz[0] != '\0') { + hybbx_log_info("[baycom%u] frequency_mhz=%s", index + 1, + inst->config.frequency_mhz); + } + + rc = instance_connect_circuit(inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + rc = baycom_modem_open(&inst->modem, &inst->config, on_modem_ax25_frame, + inst); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom%u] modem open failed (%s)", index + 1, + hybbx_result_name(rc)); + instance_shutdown(inst); + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t baycom_init(hybbx_service_t *service) +{ + g_service = service; + g_instance_count = 0; + g_running = 0; + return HYBBX_OK; +} + +static void baycom_shutdown(void) +{ + unsigned i; + + g_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + for (i = 0; i < g_instance_count; i++) { + instance_shutdown(&g_instances[i]); + } + g_instance_count = 0; +} + +static hybbx_result_t baycom_start(const char *config) +{ + hybbx_max25_config_t max25; + hybbx_result_t rc = HYBBX_OK; + unsigned started = 0; + const char *cursor; + const char *cursor_after_max25; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + baycom_shutdown(); + g_poll_thread = (pthread_t)0; + + cursor_after_max25 = hybbx_max25_config_skip_prefix(config, &max25); + if (max25.check && cursor_after_max25 != config) { + rc = hybbx_max25_wait_ready(&max25); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] local RF requires max25d — continuing without BayCom"); + return HYBBX_OK; + } + } + + cursor = cursor_after_max25; + while (*cursor != '\0' && g_instance_count < HYBBX_BAYCOM_MAX_INSTANCES) { + char scratch[4096]; + const char *sep = strchr(cursor, HYBBX_BAYCOM_INSTANCE_SEP); + size_t chunk_len; + + if (sep != NULL) { + chunk_len = (size_t)(sep - cursor); + } else { + chunk_len = strlen(cursor); + } + + if (chunk_len >= sizeof(scratch)) { + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, cursor, chunk_len); + scratch[chunk_len] = '\0'; + + rc = instance_start(&g_instances[g_instance_count], scratch, + g_instance_count); + if (rc == HYBBX_OK) { + g_instance_count++; + started++; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + if (started == 0) { + /* Soft-fail: keep HyBBX up (TNCs/telnet/…) if BayCom/based open fails. */ + hybbx_log_warn("[baycom] no instances started (%s) — continuing without BayCom", + rc != HYBBX_OK ? hybbx_result_name(rc) : "invalid"); + return HYBBX_OK; + } + + g_running = 1; + if (pthread_create(&g_poll_thread, NULL, baycom_poll_thread, NULL) != 0) { + baycom_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + return HYBBX_OK; +} + +static hybbx_result_t baycom_stop(void) +{ + baycom_shutdown(); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_baycom = { + .name = "baycom", + .kind = HYBBX_TRANSPORT_BAYCOM, + .version = 1, + .init = baycom_init, + .shutdown = baycom_shutdown, + .start = baycom_start, + .stop = baycom_stop, + .write = NULL, +}; diff --git a/plugins/baycom/baycom_config.c b/plugins/baycom/baycom_config.c new file mode 100644 index 0000000..4e740f1 --- /dev/null +++ b/plugins/baycom/baycom_config.c @@ -0,0 +1,395 @@ +#include "hybbx/baycom.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" + +#include +#include +#include + +static char *baycom_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static char *baycom_normalize_kiss_path(const char *device) +{ + static const struct { + const char *from; + const char *to; + } map[] = { + {"/tmp/bcpr/", "/tmp/max25-bcpr/"}, + {"/tmp/bcpr", "/tmp/max25-bcpr"}, + {"/var/run/bcpr/", "/tmp/max25-bcpr/"}, + {"/var/run/bcpr", "/tmp/max25-bcpr"}, + }; + size_t i; + size_t flen; + char *norm; + + if (device == NULL || device[0] == '\0') { + return NULL; + } + + for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) { + flen = strlen(map[i].from); + if (strncmp(device, map[i].from, flen) != 0) { + continue; + } + if (device[flen] != '\0' && device[flen] != '/') { + continue; + } + norm = malloc(strlen(map[i].to) + strlen(device + flen) + 1); + if (norm == NULL) { + return baycom_strdup(device); + } + snprintf(norm, strlen(map[i].to) + strlen(device + flen) + 1, "%s%s", + map[i].to, device + flen); + return norm; + } + + return baycom_strdup(device); +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +const char *hybbx_baycom_backend_name(hybbx_baycom_backend_t backend) +{ + switch (backend) { + case HYBBX_BAYCOM_BACKEND_KISS: + return "kiss"; + case HYBBX_BAYCOM_BACKEND_KERNEL: + default: + return "kernel"; + } +} + +const char *hybbx_baycom_modem_mode_name(hybbx_baycom_modem_mode_t mode) +{ + switch (mode) { + case HYBBX_BAYCOM_MODE_SER12_SOFTDCD: + return "ser12*"; + case HYBBX_BAYCOM_MODE_SER12_INV: + return "ser12+"; + case HYBBX_BAYCOM_MODE_SER12HDX: + return "ser12hdx"; + case HYBBX_BAYCOM_MODE_PAR96: + return "par96"; + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + return "par96*"; + case HYBBX_BAYCOM_MODE_EPP: + return "epp"; + case HYBBX_BAYCOM_MODE_SER12: + default: + return "ser12"; + } +} + +static hybbx_baycom_backend_t parse_backend(const char *value) +{ + if (value == NULL || str_ieq(value, "kernel") || str_ieq(value, "hdlc") || + str_ieq(value, "prstack") || str_ieq(value, "l2")) { + return HYBBX_BAYCOM_BACKEND_KERNEL; + } + + if (str_ieq(value, "kiss") || str_ieq(value, "serial")) { + return HYBBX_BAYCOM_BACKEND_KISS; + } + + return HYBBX_BAYCOM_BACKEND_KERNEL; +} + +static hybbx_baycom_modem_mode_t parse_mode(const char *value) +{ + if (value == NULL || str_ieq(value, "ser12")) { + return HYBBX_BAYCOM_MODE_SER12; + } + if (str_ieq(value, "ser12*") || str_ieq(value, "ser12_softdcd")) { + return HYBBX_BAYCOM_MODE_SER12_SOFTDCD; + } + if (str_ieq(value, "ser12+") || str_ieq(value, "ser12_inv")) { + return HYBBX_BAYCOM_MODE_SER12_INV; + } + if (str_ieq(value, "ser12hdx") || str_ieq(value, "ser12_hdx") || + str_ieq(value, "hdx")) { + return HYBBX_BAYCOM_MODE_SER12HDX; + } + if (str_ieq(value, "par96")) { + return HYBBX_BAYCOM_MODE_PAR96; + } + if (str_ieq(value, "par96*") || str_ieq(value, "par96_softdcd")) { + return HYBBX_BAYCOM_MODE_PAR96_SOFTDCD; + } + if (str_ieq(value, "epp") || str_ieq(value, "par97") || + str_ieq(value, "baycom_epp")) { + return HYBBX_BAYCOM_MODE_EPP; + } + + return HYBBX_BAYCOM_MODE_SER12_SOFTDCD; +} + +void hybbx_baycom_config_free(hybbx_baycom_config_t *config) +{ + if (config == NULL) { + return; + } + + free(config->interface); + free(config->kernel_module); + free(config->device); + free(config->mycall); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + free(config->frequency_mhz); + memset(config, 0, sizeof(*config)); +} + +static void apply_mode_defaults(hybbx_baycom_config_t *out) +{ + if (out->interface == NULL) { + switch (out->mode) { + case HYBBX_BAYCOM_MODE_SER12HDX: + out->interface = baycom_strdup("bcsh0"); + break; + case HYBBX_BAYCOM_MODE_PAR96: + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + out->interface = baycom_strdup("bcp0"); + break; + case HYBBX_BAYCOM_MODE_EPP: + out->interface = baycom_strdup("bce0"); + break; + case HYBBX_BAYCOM_MODE_SER12: + case HYBBX_BAYCOM_MODE_SER12_SOFTDCD: + case HYBBX_BAYCOM_MODE_SER12_INV: + default: + out->interface = baycom_strdup(HYBBX_BAYCOM_DEFAULT_INTERFACE); + break; + } + } + + if (out->kernel_module == NULL) { + switch (out->mode) { + case HYBBX_BAYCOM_MODE_SER12HDX: + out->kernel_module = baycom_strdup("baycom_ser_hdx"); + break; + case HYBBX_BAYCOM_MODE_PAR96: + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + out->kernel_module = baycom_strdup("baycom_par"); + break; + case HYBBX_BAYCOM_MODE_EPP: + out->kernel_module = baycom_strdup("baycom_epp"); + break; + default: + out->kernel_module = baycom_strdup("baycom_ser_fdx"); + break; + } + } + + if (out->device == NULL) { + if (out->backend == HYBBX_BAYCOM_BACKEND_KISS) { + out->device = baycom_strdup("/tmp/max25-bcpr/kiss-bc0"); + } else { + out->device = baycom_strdup("/dev/ttyS0"); + } + } +} + +hybbx_result_t hybbx_baycom_config_parse(const char *config, + hybbx_baycom_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_baycom_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_kv(config, "backend", scratch, sizeof(scratch)); + out->backend = parse_backend(value); + + value = find_kv(config, "mode", scratch, sizeof(scratch)); + out->mode = parse_mode(value); + + value = find_kv(config, "interface", scratch, sizeof(scratch)); + if (value != NULL) { + out->interface = baycom_strdup(value); + } + + value = find_kv(config, "kernel_module", scratch, sizeof(scratch)); + if (value != NULL) { + out->kernel_module = baycom_strdup(value); + } + + value = find_kv(config, "iobase", scratch, sizeof(scratch)); + out->iobase = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_BAYCOM_DEFAULT_IOBASE; + + value = find_kv(config, "irq", scratch, sizeof(scratch)); + out->irq = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_BAYCOM_DEFAULT_IRQ; + + value = find_kv(config, "radio_baud", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "baud", scratch, sizeof(scratch)); + } + out->radio_baud = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_BAYCOM_DEFAULT_RADIO_BAUD; + + value = find_kv(config, "kernel_autoload", scratch, sizeof(scratch)); + out->kernel_autoload = value != NULL ? hybbx_parse_bool(value, 0) : 0; + + value = find_kv(config, "txdelay", scratch, sizeof(scratch)); + out->txdelay = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 30u; + + value = find_kv(config, "txtail", scratch, sizeof(scratch)); + out->txtail = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 1u; + + value = find_kv(config, "slot", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "slottime", scratch, sizeof(scratch)); + } + out->slot = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 10u; + + value = find_kv(config, "persist", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "ppersist", scratch, sizeof(scratch)); + } + out->persist = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 128u; + + value = find_kv(config, "full_duplex", scratch, sizeof(scratch)); + if (value != NULL) { + out->full_duplex = hybbx_parse_bool(value, 0); + } else { + value = find_kv(config, "duplex", scratch, sizeof(scratch)); + out->full_duplex = value != NULL && str_ieq(value, "full"); + } + + value = find_kv(config, "device", scratch, sizeof(scratch)); + if (value != NULL) { + out->device = baycom_strdup(value); + } + + value = find_kv(config, "serial_baud", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "host_baud", scratch, sizeof(scratch)); + } + out->serial_baud = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 1200u; + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + if (value != NULL) { + out->mycall = baycom_strdup(value); + } + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = baycom_strdup(value != NULL ? value : "127.0.0.1"); + + value = find_kv(config, "circuit_port", scratch, sizeof(scratch)); + out->circuit_port = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_CIRCUIT_DEFAULT_PORT; + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = baycom_strdup(value != NULL ? value : "baycom-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + out->link_password = baycom_strdup(value != NULL ? value : ""); + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = baycom_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value != NULL) { + out->frequency_mhz = baycom_strdup(value); + } + + apply_mode_defaults(out); + + if (out->backend == HYBBX_BAYCOM_BACKEND_KISS && out->device != NULL) { + char *norm = baycom_normalize_kiss_path(out->device); + + if (norm != NULL) { + free(out->device); + out->device = norm; + } + } + + if (out->circuit_host == NULL || out->link_id == NULL) { + hybbx_baycom_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/baycom/baycom_kernel.c b/plugins/baycom/baycom_kernel.c new file mode 100644 index 0000000..0babc08 --- /dev/null +++ b/plugins/baycom/baycom_kernel.c @@ -0,0 +1,472 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "baycom_kernel.h" + +#include "hybbx/ax25.h" +#include "hybbx/baycom.h" +#include "hybbx/log.h" +#include "hybbx/rf_tx_pace.h" + +#include +#include +#include +#include +#include + +#if defined(__linux__) + +#include +#include +#include +#include +#include +#include +#include +#include + +static char *baycom_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +struct baycom_kernel_modem { + hybbx_baycom_config_t cfg; + baycom_modem_frame_cb frame_cb; + void *frame_userdata; + int ctl_fd; + int pkt_fd; + int ifindex; +}; + +static void baycom_kernel_mode_string(const hybbx_baycom_config_t *cfg, + char *out, size_t out_cap) +{ + unsigned rate; + char suffix = '\0'; + + if (out == NULL || out_cap == 0 || cfg == NULL) { + return; + } + + switch (cfg->mode) { + case HYBBX_BAYCOM_MODE_SER12_SOFTDCD: + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + suffix = '*'; + break; + case HYBBX_BAYCOM_MODE_SER12_INV: + suffix = '+'; + break; + default: + break; + } + + if (cfg->mode == HYBBX_BAYCOM_MODE_PAR96 || + cfg->mode == HYBBX_BAYCOM_MODE_PAR96_SOFTDCD) { + snprintf(out, out_cap, "par96%c", suffix); + return; + } + + if (cfg->mode == HYBBX_BAYCOM_MODE_EPP) { + snprintf(out, out_cap, "epp"); + return; + } + + rate = cfg->radio_baud / 100u; + if (rate == 0) { + rate = 12; + } + snprintf(out, out_cap, "ser%u%c", rate, suffix); +} + +static int baycom_kernel_hdlc_ioctl(baycom_kernel_modem_t *modem, + struct hdlcdrv_ioctl *hi) +{ + struct ifreq ifr; + + if (modem == NULL || hi == NULL || modem->ctl_fd < 0 || + modem->cfg.interface == NULL) { + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + ifr.ifr_data = (void *)hi; + + if (ioctl(modem->ctl_fd, SIOCDEVPRIVATE, &ifr) < 0) { + return -1; + } + + return 0; +} + +static int baycom_kernel_interface_flags(baycom_kernel_modem_t *modem, + unsigned short *flags_out) +{ + struct ifreq ifr; + + if (modem == NULL || flags_out == NULL || modem->ctl_fd < 0) { + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + if (ioctl(modem->ctl_fd, SIOCGIFFLAGS, &ifr) < 0) { + return -1; + } + + *flags_out = ifr.ifr_flags; + return 0; +} + +static int baycom_kernel_interface_up(baycom_kernel_modem_t *modem, int up) +{ + struct ifreq ifr; + unsigned short flags = 0; + + if (baycom_kernel_interface_flags(modem, &flags) < 0) { + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + ifr.ifr_flags = flags; + if (up) { + ifr.ifr_flags |= IFF_UP; + } else { + ifr.ifr_flags &= (unsigned short)~IFF_UP; + } + + return ioctl(modem->ctl_fd, SIOCSIFFLAGS, &ifr); +} + +static int baycom_kernel_set_modem_params(baycom_kernel_modem_t *modem) +{ + struct hdlcdrv_ioctl hi; + + memset(&hi, 0, sizeof(hi)); + hi.cmd = HDLCDRVCTL_SETMODEMPAR; + hi.data.mp.iobase = (int)modem->cfg.iobase; + hi.data.mp.irq = (int)modem->cfg.irq; + + return baycom_kernel_hdlc_ioctl(modem, &hi); +} + +static int baycom_kernel_set_mode(baycom_kernel_modem_t *modem) +{ + struct hdlcdrv_ioctl hi; + char modestr[32]; + + baycom_kernel_mode_string(&modem->cfg, modestr, sizeof(modestr)); + + memset(&hi, 0, sizeof(hi)); + hi.cmd = HDLCDRVCTL_SETMODE; + strncpy(hi.data.modename, modestr, sizeof(hi.data.modename) - 1); + + return baycom_kernel_hdlc_ioctl(modem, &hi); +} + +static int baycom_kernel_set_channel_params(baycom_kernel_modem_t *modem) +{ + struct hdlcdrv_ioctl hi; + + memset(&hi, 0, sizeof(hi)); + hi.cmd = HDLCDRVCTL_SETCHANNELPAR; + hi.data.cp.tx_delay = (int)modem->cfg.txdelay; + hi.data.cp.tx_tail = (int)modem->cfg.txtail; + hi.data.cp.slottime = (int)modem->cfg.slot; + hi.data.cp.ppersist = (int)modem->cfg.persist; + hi.data.cp.fulldup = modem->cfg.full_duplex ? 1 : 0; + + return baycom_kernel_hdlc_ioctl(modem, &hi); +} + +static int baycom_kernel_modprobe(const hybbx_baycom_config_t *cfg) +{ + char cmd[384]; + char modestr[32]; + int rc; + + if (cfg == NULL || cfg->kernel_module == NULL) { + return -1; + } + + baycom_kernel_mode_string(cfg, modestr, sizeof(modestr)); + snprintf(cmd, sizeof(cmd), + "modprobe %s mode=%s iobase=0x%x irq=%u baud=%u 2>/dev/null", + cfg->kernel_module, modestr, cfg->iobase, cfg->irq, + cfg->radio_baud); + + rc = system(cmd); + if (rc != 0) { + hybbx_log_warn("[baycom] modprobe failed (%s)", cfg->kernel_module); + return -1; + } + + return 0; +} + +static int baycom_kernel_ifindex(baycom_kernel_modem_t *modem) +{ + struct ifreq ifr; + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + if (ioctl(modem->ctl_fd, SIOCGIFINDEX, &ifr) < 0) { + return -1; + } + + modem->ifindex = ifr.ifr_ifindex; + return 0; +} + +static int baycom_kernel_open_packet(baycom_kernel_modem_t *modem) +{ + struct sockaddr_ll bind_addr; + + modem->pkt_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_AX25)); + if (modem->pkt_fd < 0) { + return -1; + } + + if (baycom_kernel_ifindex(modem) < 0) { + return -1; + } + + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sll_family = AF_PACKET; + bind_addr.sll_protocol = htons(ETH_P_AX25); + bind_addr.sll_ifindex = modem->ifindex; + + if (bind(modem->pkt_fd, (struct sockaddr *)&bind_addr, + sizeof(bind_addr)) < 0) { + return -1; + } + + return 0; +} + +hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + baycom_kernel_modem_t *km; + unsigned short flags = 0; + + if (out == NULL || cfg == NULL || cfg->interface == NULL) { + return HYBBX_ERR_INVALID; + } + + *out = NULL; + km = calloc(1, sizeof(*km)); + if (km == NULL) { + return HYBBX_ERR_NOMEM; + } + + /* Own string copies only — avoid double-free with caller config. */ + memset(&km->cfg, 0, sizeof(km->cfg)); + km->cfg.backend = cfg->backend; + km->cfg.mode = cfg->mode; + km->cfg.iobase = cfg->iobase; + km->cfg.irq = cfg->irq; + km->cfg.radio_baud = cfg->radio_baud; + km->cfg.kernel_autoload = cfg->kernel_autoload; + km->cfg.txdelay = cfg->txdelay; + km->cfg.txtail = cfg->txtail; + km->cfg.slot = cfg->slot; + km->cfg.persist = cfg->persist; + km->cfg.full_duplex = cfg->full_duplex; + km->cfg.serial_baud = cfg->serial_baud; + km->cfg.circuit_port = cfg->circuit_port; + km->cfg.interface = baycom_strdup(cfg->interface); + km->cfg.kernel_module = baycom_strdup(cfg->kernel_module); + km->cfg.device = baycom_strdup(cfg->device); + km->cfg.mycall = baycom_strdup(cfg->mycall); + km->cfg.circuit_host = baycom_strdup(cfg->circuit_host); + km->cfg.link_id = baycom_strdup(cfg->link_id); + km->cfg.link_password = baycom_strdup(cfg->link_password); + km->cfg.link_role = baycom_strdup(cfg->link_role); + km->cfg.frequency_mhz = baycom_strdup(cfg->frequency_mhz); + km->frame_cb = cb; + km->frame_userdata = userdata; + km->ctl_fd = -1; + km->pkt_fd = -1; + + if (km->cfg.interface == NULL) { + free(km); + return HYBBX_ERR_NOMEM; + } + + if (cfg->kernel_autoload) { + if (baycom_kernel_modprobe(cfg) < 0) { + hybbx_baycom_config_free(&km->cfg); + free(km); + return HYBBX_ERR_IO; + } + } + + km->ctl_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (km->ctl_fd < 0) { + hybbx_baycom_config_free(&km->cfg); + free(km); + return HYBBX_ERR_IO; + } + + if (baycom_kernel_interface_flags(km, &flags) < 0) { + hybbx_log_warn("[baycom] interface %s not found (load %s first)", + km->cfg.interface, + km->cfg.kernel_module != NULL ? km->cfg.kernel_module + : "baycom module"); + baycom_kernel_close(km); + return HYBBX_ERR_IO; + } + + if (!(flags & IFF_UP)) { + if (baycom_kernel_set_modem_params(km) < 0) { + hybbx_log_warn("[baycom] SETMODEMPAR failed (need CAP_SYS_RAWIO, iface down)"); + } + if (baycom_kernel_set_mode(km) < 0) { + hybbx_log_warn("[baycom] SETMODE failed (need CAP_NET_ADMIN, iface down)"); + } + if (baycom_kernel_interface_up(km, 1) < 0) { + hybbx_log_warn("[baycom] failed to bring %s up", km->cfg.interface); + baycom_kernel_close(km); + return HYBBX_ERR_IO; + } + } + + if (baycom_kernel_set_channel_params(km) < 0) { + hybbx_log_warn("[baycom] channel params ioctl failed"); + } + + if (baycom_kernel_open_packet(km) < 0) { + baycom_kernel_close(km); + return HYBBX_ERR_IO; + } + + *out = km; + return HYBBX_OK; +} + +void baycom_kernel_close(baycom_kernel_modem_t *km) +{ + if (km == NULL) { + return; + } + + if (km->pkt_fd >= 0) { + close(km->pkt_fd); + } + if (km->ctl_fd >= 0) { + close(km->ctl_fd); + } + + hybbx_baycom_config_free(&km->cfg); + free(km); +} + +hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *km) +{ + uint8_t buf[HYBBX_AX25_FRAME_MAX + 64]; + ssize_t n; + + if (km == NULL || km->pkt_fd < 0) { + return HYBBX_ERR_IO; + } + + for (;;) { + n = recv(km->pkt_fd, buf, sizeof(buf), MSG_DONTWAIT); + if (n < 0) { + if (errno == EAGAIN +#if EAGAIN != EWOULDBLOCK + || errno == EWOULDBLOCK +#endif + ) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_OK; + } + + if (km->frame_cb != NULL && (size_t)n > 0) { + km->frame_cb(buf, (size_t)n, km->frame_userdata); + } + } +} + +hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *km, + const uint8_t *frame, size_t len) +{ + struct sockaddr_ll dest; + ssize_t n; + + if (km == NULL || frame == NULL || len == 0 || km->pkt_fd < 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_rf_tx_pace(); + + memset(&dest, 0, sizeof(dest)); + dest.sll_family = AF_PACKET; + dest.sll_protocol = htons(ETH_P_AX25); + dest.sll_ifindex = km->ifindex; + dest.sll_halen = 0; + + n = sendto(km->pkt_fd, frame, len, 0, (struct sockaddr *)&dest, + sizeof(dest)); + if (n < 0 || (size_t)n != len) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +#else /* !__linux__ */ + +hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + (void)out; + (void)cfg; + (void)cb; + (void)userdata; + return HYBBX_ERR_UNSUPPORTED; +} + +void baycom_kernel_close(baycom_kernel_modem_t *modem) +{ + (void)modem; +} + +hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *modem) +{ + (void)modem; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *modem, + const uint8_t *frame, size_t len) +{ + (void)modem; + (void)frame; + (void)len; + return HYBBX_ERR_UNSUPPORTED; +} + +#endif /* __linux__ */ diff --git a/plugins/baycom/baycom_kernel.h b/plugins/baycom/baycom_kernel.h new file mode 100644 index 0000000..b35e3b9 --- /dev/null +++ b/plugins/baycom/baycom_kernel.h @@ -0,0 +1,26 @@ +#ifndef BAYCOM_KERNEL_H +#define BAYCOM_KERNEL_H + +#include "baycom_modem.h" +#include "hybbx/baycom.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct baycom_kernel_modem baycom_kernel_modem_t; + +hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata); +void baycom_kernel_close(baycom_kernel_modem_t *modem); +hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *modem); +hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *modem, + const uint8_t *frame, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* BAYCOM_KERNEL_H */ diff --git a/plugins/baycom/baycom_kiss.c b/plugins/baycom/baycom_kiss.c new file mode 100644 index 0000000..c66b220 --- /dev/null +++ b/plugins/baycom/baycom_kiss.c @@ -0,0 +1,262 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "baycom_kiss.h" + +#include "hybbx/kiss.h" +#include "hybbx/rf_tx_pace.h" +#include "serial_port.h" +#include "hybbx/log.h" +#include "hybbx/limits.h" + +#include +#include +#include +#include +#include + +static hybbx_result_t kiss_wait_device(const char *path) +{ + unsigned waited_ms = 0; + const unsigned step_ms = 500u; + const unsigned max_wait_ms = 30000u; + + if (path == NULL || path[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (access(path, F_OK) == 0) { + return HYBBX_OK; + } + + while (access(path, F_OK) != 0) { + if (waited_ms >= max_wait_ms) { + hybbx_log_warn("[baycom] kiss device not ready: %s", path); + return HYBBX_ERR_IO; + } + if (waited_ms == 0u) { + hybbx_log_info("[baycom] waiting for kiss device %s (up to %u s)", + path, max_wait_ms / 1000u); + } + usleep((useconds_t)step_ms * 1000u); + waited_ms += step_ms; + } + + return HYBBX_OK; +} + +struct baycom_kiss_modem { + hybbx_baycom_config_t cfg; + hybbx_serial_port_t *serial; + hybbx_kiss_decoder_t kiss_dec; + baycom_modem_frame_cb frame_cb; + void *frame_userdata; +}; + +static char *baycom_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static void on_kiss_frame(uint8_t port, const uint8_t *frame, size_t len, + void *userdata) +{ + baycom_kiss_modem_t *km = (baycom_kiss_modem_t *)userdata; + + (void)port; + + if (km == NULL || km->frame_cb == NULL || len == 0) { + return; + } + + km->frame_cb(frame, len, km->frame_userdata); +} + +static hybbx_result_t kiss_send_param(baycom_kiss_modem_t *km, + hybbx_kiss_command_t cmd, uint8_t value) +{ + uint8_t out[16]; + size_t out_len; + + out_len = hybbx_kiss_encode(0, cmd, &value, 1, out, sizeof(out)); + if (out_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_serial_write(km->serial, out, out_len); +} + +static hybbx_result_t kiss_apply_channel_params(baycom_kiss_modem_t *km) +{ + hybbx_result_t rc; + + rc = kiss_send_param(km, HYBBX_KISS_CMD_TXDELAY, (uint8_t)km->cfg.txdelay); + if (rc != HYBBX_OK) { + return rc; + } + rc = kiss_send_param(km, HYBBX_KISS_CMD_PERSIST, (uint8_t)km->cfg.persist); + if (rc != HYBBX_OK) { + return rc; + } + rc = kiss_send_param(km, HYBBX_KISS_CMD_SLOTTIME, (uint8_t)km->cfg.slot); + if (rc != HYBBX_OK) { + return rc; + } + rc = kiss_send_param(km, HYBBX_KISS_CMD_TXTAIL, (uint8_t)km->cfg.txtail); + if (rc != HYBBX_OK) { + return rc; + } + return kiss_send_param(km, HYBBX_KISS_CMD_FULLDUPLEX, + km->cfg.full_duplex ? 1u : 0u); +} + +hybbx_result_t baycom_kiss_open(baycom_kiss_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + baycom_kiss_modem_t *km; + hybbx_serial_params_t sparams; + hybbx_result_t rc; + + if (out == NULL || cfg == NULL || cfg->device == NULL) { + return HYBBX_ERR_INVALID; + } + + *out = NULL; + km = calloc(1, sizeof(*km)); + if (km == NULL) { + return HYBBX_ERR_NOMEM; + } + + /* Own string copies only — never shallow-copy pointers from caller config + * (hybbx_baycom_config_free on close would double-free inst->config). */ + memset(&km->cfg, 0, sizeof(km->cfg)); + km->cfg.backend = cfg->backend; + km->cfg.mode = cfg->mode; + km->cfg.iobase = cfg->iobase; + km->cfg.irq = cfg->irq; + km->cfg.radio_baud = cfg->radio_baud; + km->cfg.kernel_autoload = cfg->kernel_autoload; + km->cfg.txdelay = cfg->txdelay; + km->cfg.txtail = cfg->txtail; + km->cfg.slot = cfg->slot; + km->cfg.persist = cfg->persist; + km->cfg.full_duplex = cfg->full_duplex; + km->cfg.serial_baud = cfg->serial_baud; + km->cfg.circuit_port = cfg->circuit_port; + km->cfg.device = baycom_strdup(cfg->device); + km->cfg.mycall = baycom_strdup(cfg->mycall); + km->cfg.circuit_host = baycom_strdup(cfg->circuit_host); + km->cfg.link_id = baycom_strdup(cfg->link_id); + km->cfg.link_password = baycom_strdup(cfg->link_password); + km->cfg.link_role = baycom_strdup(cfg->link_role); + km->cfg.frequency_mhz = baycom_strdup(cfg->frequency_mhz); + km->frame_cb = cb; + km->frame_userdata = userdata; + hybbx_kiss_decoder_init(&km->kiss_dec); + + hybbx_serial_params_default(&sparams, cfg->serial_baud); + sparams.data_bits = 8; + sparams.parity = HYBBX_SERIAL_PARITY_NONE; + sparams.stop_bits = 1; + sparams.assert_modem_lines = 0; + + rc = kiss_wait_device(km->cfg.device); + if (rc != HYBBX_OK) { + baycom_kiss_close(km); + return rc; + } + + if (access(km->cfg.device, R_OK | W_OK) != 0) { + hybbx_log_warn("[baycom] kiss device not readable/writable: %s (%s; user in dialout?)", + km->cfg.device, strerror(errno)); + baycom_kiss_close(km); + return HYBBX_ERR_IO; + } + + rc = hybbx_serial_open(&km->serial, km->cfg.device, &sparams); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] kiss serial open failed: %s (see [serial] log)", + km->cfg.device); + baycom_kiss_close(km); + return rc; + } + + rc = kiss_apply_channel_params(km); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] kiss channel params failed"); + } + + *out = km; + return HYBBX_OK; +} + +void baycom_kiss_close(baycom_kiss_modem_t *km) +{ + if (km == NULL) { + return; + } + + if (km->serial != NULL) { + hybbx_serial_close(km->serial); + } + + hybbx_baycom_config_free(&km->cfg); + free(km); +} + +hybbx_result_t baycom_kiss_poll(baycom_kiss_modem_t *km) +{ + uint8_t buf[256]; + size_t read_len = 0; + hybbx_result_t rc; + + if (km == NULL || km->serial == NULL) { + return HYBBX_ERR_IO; + } + + rc = hybbx_serial_read(km->serial, buf, sizeof(buf), &read_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (read_len > 0) { + hybbx_kiss_decoder_feed(&km->kiss_dec, buf, read_len, on_kiss_frame, km); + } + + return HYBBX_OK; +} + +hybbx_result_t baycom_kiss_send_frame(baycom_kiss_modem_t *km, + const uint8_t *frame, size_t len) +{ + uint8_t out[HYBBX_KISS_MAX_FRAME + 8]; + size_t out_len; + + if (km == NULL || km->serial == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_rf_tx_burst_guard(); + + out_len = hybbx_kiss_encode(0, HYBBX_KISS_CMD_DATA, frame, len, + out, sizeof(out)); + if (out_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_serial_write(km->serial, out, out_len); +} diff --git a/plugins/baycom/baycom_kiss.h b/plugins/baycom/baycom_kiss.h new file mode 100644 index 0000000..49c7b36 --- /dev/null +++ b/plugins/baycom/baycom_kiss.h @@ -0,0 +1,26 @@ +#ifndef BAYCOM_KISS_H +#define BAYCOM_KISS_H + +#include "baycom_modem.h" +#include "hybbx/baycom.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct baycom_kiss_modem baycom_kiss_modem_t; + +hybbx_result_t baycom_kiss_open(baycom_kiss_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata); +void baycom_kiss_close(baycom_kiss_modem_t *modem); +hybbx_result_t baycom_kiss_poll(baycom_kiss_modem_t *modem); +hybbx_result_t baycom_kiss_send_frame(baycom_kiss_modem_t *modem, + const uint8_t *frame, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* BAYCOM_KISS_H */ diff --git a/plugins/baycom/baycom_modem.c b/plugins/baycom/baycom_modem.c new file mode 100644 index 0000000..e4e332f --- /dev/null +++ b/plugins/baycom/baycom_modem.c @@ -0,0 +1,94 @@ +#include "baycom_modem.h" +#include "baycom_kernel.h" +#include "baycom_kiss.h" + +#include + +struct baycom_modem { + hybbx_baycom_backend_t backend; + baycom_kernel_modem_t *kernel; + baycom_kiss_modem_t *kiss; +}; + +hybbx_result_t baycom_modem_open(baycom_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + baycom_modem_t *modem; + hybbx_result_t rc; + + if (out == NULL || cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + *out = NULL; + modem = calloc(1, sizeof(*modem)); + if (modem == NULL) { + return HYBBX_ERR_NOMEM; + } + + modem->backend = cfg->backend; + + if (cfg->backend == HYBBX_BAYCOM_BACKEND_KISS) { + rc = baycom_kiss_open(&modem->kiss, cfg, cb, userdata); + } else { + rc = baycom_kernel_open(&modem->kernel, cfg, cb, userdata); + } + + if (rc != HYBBX_OK) { + baycom_modem_close(modem); + return rc; + } + + *out = modem; + return HYBBX_OK; +} + +void baycom_modem_close(baycom_modem_t *modem) +{ + if (modem == NULL) { + return; + } + + if (modem->kernel != NULL) { + baycom_kernel_close(modem->kernel); + } + if (modem->kiss != NULL) { + baycom_kiss_close(modem->kiss); + } + + free(modem); +} + +hybbx_result_t baycom_modem_poll(baycom_modem_t *modem) +{ + if (modem == NULL) { + return HYBBX_ERR_INVALID; + } + + if (modem->kiss != NULL) { + return baycom_kiss_poll(modem->kiss); + } + if (modem->kernel != NULL) { + return baycom_kernel_poll(modem->kernel); + } + + return HYBBX_ERR_IO; +} + +hybbx_result_t baycom_modem_send_frame(baycom_modem_t *modem, + const uint8_t *frame, size_t len) +{ + if (modem == NULL) { + return HYBBX_ERR_INVALID; + } + + if (modem->kiss != NULL) { + return baycom_kiss_send_frame(modem->kiss, frame, len); + } + if (modem->kernel != NULL) { + return baycom_kernel_send_frame(modem->kernel, frame, len); + } + + return HYBBX_ERR_IO; +} diff --git a/plugins/baycom/baycom_modem.h b/plugins/baycom/baycom_modem.h new file mode 100644 index 0000000..2365636 --- /dev/null +++ b/plugins/baycom/baycom_modem.h @@ -0,0 +1,31 @@ +#ifndef BAYCOM_MODEM_H +#define BAYCOM_MODEM_H + +#include "hybbx/baycom.h" +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*baycom_modem_frame_cb)(const uint8_t *frame, size_t len, + void *userdata); + +typedef struct baycom_modem baycom_modem_t; + +hybbx_result_t baycom_modem_open(baycom_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata); +void baycom_modem_close(baycom_modem_t *modem); +hybbx_result_t baycom_modem_poll(baycom_modem_t *modem); +hybbx_result_t baycom_modem_send_frame(baycom_modem_t *modem, + const uint8_t *frame, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* BAYCOM_MODEM_H */ diff --git a/plugins/crdop/CMakeLists.txt b/plugins/crdop/CMakeLists.txt new file mode 100644 index 0000000..03c4f1d --- /dev/null +++ b/plugins/crdop/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(hybbx_plugin_crdop STATIC + crdop.c + crdop_config.c +) + +target_include_directories(hybbx_plugin_crdop + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/plugins/ardop + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_crdop + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_crdop PRIVATE hybbx_ardop_common) +hybbx_link_plugin_instances(hybbx_plugin_crdop HYBBX_HAVE_PLUGIN_CRDOP) +hybbx_apply_hardening(hybbx_plugin_crdop) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_crdop + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/crdop/crdop.c b/plugins/crdop/crdop.c new file mode 100644 index 0000000..d2c6f3d --- /dev/null +++ b/plugins/crdop/crdop.c @@ -0,0 +1,51 @@ +/* + * crdop — CB Radio Digital Open Protocol host-client (external ARDOPC/ardopcf). + * INI: [transport.crdop] or [transport.crdopN]. No embedded modem in HyBBX. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" + +#include "../ardop/ardop_link.h" + +static const ardop_link_plugin_t g_crdop_link = { + .name = "crdop", + .kind = HYBBX_TRANSPORT_CRDOP, + .default_link_id = "crdop-link", + .modem_hint = "CRDOPC (ARDOP-compatible host TCP)", + .parse_config = (ardop_link_config_fn)hybbx_crdop_config_parse, + .config_free = (void (*)(void *))hybbx_ardop_config_free, + .config_size = sizeof(hybbx_ardop_config_t), +}; + +static hybbx_result_t crdop_init(hybbx_service_t *service) +{ + return ardop_link_init(service, &g_crdop_link); +} + +static void crdop_shutdown(void) +{ + ardop_link_shutdown(&g_crdop_link); +} + +static hybbx_result_t crdop_start(const char *config) +{ + return ardop_link_start(&g_crdop_link, config); +} + +static hybbx_result_t crdop_stop(void) +{ + return ardop_link_stop(&g_crdop_link); +} + +const hybbx_transport_plugin_t hybbx_plugin_crdop = { + .name = "crdop", + .kind = HYBBX_TRANSPORT_CRDOP, + .version = 1, + .init = crdop_init, + .shutdown = crdop_shutdown, + .start = crdop_start, + .stop = crdop_stop, + .write = NULL, +}; diff --git a/plugins/crdop/crdop_config.c b/plugins/crdop/crdop_config.c new file mode 100644 index 0000000..e1393bd --- /dev/null +++ b/plugins/crdop/crdop_config.c @@ -0,0 +1,186 @@ +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" +#include "hybbx/log.h" + +#include +#include +#include + +static char *crdop_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +static const char *find_host(const char *config, char *scratch, size_t len) +{ + const char *value; + + value = find_kv(config, "modem_host", scratch, len); + if (value != NULL && value[0] != '\0') { + return value; + } + return find_kv(config, "ardop_host", scratch, len); +} + +static const char *find_port_str(const char *config, char *scratch, size_t len) +{ + const char *value; + + value = find_kv(config, "modem_port", scratch, len); + if (value != NULL && value[0] != '\0') { + return value; + } + return find_kv(config, "ardop_port", scratch, len); +} + +hybbx_result_t hybbx_crdop_config_parse(const char *config, + hybbx_ardop_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ardop_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_host(config, scratch, sizeof(scratch)); + out->ardop_host = crdop_strdup(value != NULL ? value : "127.0.0.1"); + + out->ardop_port = (unsigned)strtoul( + find_port_str(config, scratch, sizeof(scratch)) != NULL ? + scratch : "8515", NULL, 10); + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + out->mycall = crdop_strdup(value != NULL ? value : "NOCALL"); + + out->radio_profile = HYBBX_CRDOP_PROFILE_CB; + + value = find_kv(config, "arq_bandwidth", scratch, sizeof(scratch)); + out->arq_bandwidth = crdop_strdup( + value != NULL ? value : + hybbx_crdop_default_arq_bandwidth(HYBBX_CRDOP_PROFILE_CB)); + + value = find_kv(config, "peer_call", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->peer_call = crdop_strdup(value); + } + + value = find_kv(config, "listen", scratch, sizeof(scratch)); + out->listen = value == NULL || str_ieq(value, "yes") || str_ieq(value, "1") || + str_ieq(value, "true"); + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = crdop_strdup(value != NULL ? value : "127.0.0.1"); + + out->circuit_port = (unsigned)strtoul( + find_kv(config, "circuit_port", scratch, sizeof(scratch)) != NULL ? + scratch : "7323", NULL, 10); + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = crdop_strdup(value != NULL ? value : "crdop-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = crdop_strdup(value); + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = crdop_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = strtod(value, NULL); + } + + if (hybbx_crdop_bandwidth_exceeds_cb(out->arq_bandwidth)) { + hybbx_log_warn("[crdop] warning: arq_bandwidth=%s high for CB profile", + out->arq_bandwidth); + } + + if (out->ardop_host == NULL || out->mycall == NULL || + out->arq_bandwidth == NULL || out->circuit_host == NULL || + out->link_id == NULL || out->link_role == NULL) { + hybbx_ardop_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/mains_proxy/CMakeLists.txt b/plugins/mains_proxy/CMakeLists.txt new file mode 100644 index 0000000..0d2922e --- /dev/null +++ b/plugins/mains_proxy/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(hybbx_plugin_mains_proxy STATIC + mains_proxy.c +) + +target_include_directories(hybbx_plugin_mains_proxy + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_mains_proxy + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_mains_proxy PRIVATE hybbx_core) +hybbx_link_plugin_instances(hybbx_plugin_mains_proxy HYBBX_HAVE_PLUGIN_MAINS_PROXY) + +hybbx_apply_hardening(hybbx_plugin_mains_proxy) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_mains_proxy + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/mains_proxy/mains_proxy.c b/plugins/mains_proxy/mains_proxy.c new file mode 100644 index 0000000..8b6edff --- /dev/null +++ b/plugins/mains_proxy/mains_proxy.c @@ -0,0 +1,137 @@ +/* + * mains_proxy — Main-to-Main mesh proxy. + * + * Peers attach via HBX/Circuit only (circuit_host, circuit_port, link_id). + * Service linking only — proxymail and proxychat payloads; no user data. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include + +typedef struct mains_proxy_plugin_state { + hybbx_service_t *service; + hybbx_mains_proxy_mesh_t mesh; + int loaded; +} mains_proxy_plugin_state_t; + +static mains_proxy_plugin_state_t g_state; + +static hybbx_result_t mains_proxy_parse_instances(const char *config, + hybbx_mains_proxy_mesh_t *mesh) +{ + const char *cursor; + const char *end; + char section_buf[HYBBX_CONFIG_LINE_MAX]; + unsigned count = 0; + + if (mesh == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_mains_proxy_mesh_init(mesh); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + cursor = config; + while (*cursor != '\0' && count < HYBBX_MAINS_PROXY_MAX_PEERS) { + size_t len; + + end = strchr(cursor, HYBBX_MAINS_PROXY_INSTANCE_SEP); + if (end == NULL) { + len = strlen(cursor); + } else { + len = (size_t)(end - cursor); + } + + if (len >= sizeof(section_buf)) { + return HYBBX_ERR_INVALID; + } + + memcpy(section_buf, cursor, len); + section_buf[len] = '\0'; + + if (hybbx_mains_proxy_peer_parse(section_buf, &mesh->peers[count]) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + if (mesh->peers[count].enabled) { + count++; + } + + if (end == NULL) { + break; + } + cursor = end + 1; + } + + mesh->peer_count = count; + return HYBBX_OK; +} + +static hybbx_result_t mains_proxy_init(hybbx_service_t *service) +{ + memset(&g_state, 0, sizeof(g_state)); + g_state.service = service; + hybbx_mains_proxy_mesh_init(&g_state.mesh); + g_state.loaded = 1; + return HYBBX_OK; +} + +static void mains_proxy_shutdown(void) +{ + hybbx_mains_proxy_mesh_stop(&g_state.mesh); + memset(&g_state, 0, sizeof(g_state)); +} + +static hybbx_result_t mains_proxy_start(const char *config) +{ + hybbx_result_t rc; + + if (!g_state.loaded) { + return HYBBX_ERR_INVALID; + } + + rc = mains_proxy_parse_instances(config, &g_state.mesh); + if (rc != HYBBX_OK) { + hybbx_log_warn("[mains_proxy] invalid peer configuration"); + return rc; + } + + return hybbx_mains_proxy_mesh_start(g_state.service, &g_state.mesh); +} + +static hybbx_result_t mains_proxy_stop(void) +{ + hybbx_mains_proxy_mesh_stop(&g_state.mesh); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_mains_proxy = { + .name = "mains_proxy", + .kind = HYBBX_TRANSPORT_MAINS_PROXY, + .version = 1, + .init = mains_proxy_init, + .shutdown = mains_proxy_shutdown, + .start = mains_proxy_start, + .stop = mains_proxy_stop, + .write = NULL, +}; + +void hybbx_mains_proxy_plugin_tick(void) +{ + if (!g_state.loaded || !g_state.mesh.running) { + return; + } + + hybbx_mains_proxy_mesh_tick(g_state.service, &g_state.mesh); +} \ No newline at end of file diff --git a/plugins/packet_radio/CMakeLists.txt b/plugins/packet_radio/CMakeLists.txt new file mode 100644 index 0000000..1dc07a5 --- /dev/null +++ b/plugins/packet_radio/CMakeLists.txt @@ -0,0 +1,36 @@ +add_library(hybbx_plugin_packet_radio STATIC + packet_radio.c + packet_radio_config.c + ax25.c + kiss.c + serial_port.c + sixpack.c + tnc.c + tnc_host.c + tnc_profiles.c +) + +target_include_directories(hybbx_plugin_packet_radio + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_packet_radio + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_plugin_packet_radio PRIVATE hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_plugin_packet_radio PRIVATE amiga) +endif() +hybbx_link_plugin_instances(hybbx_plugin_packet_radio HYBBX_HAVE_PLUGIN_PACKET_RADIO) +hybbx_apply_hardening(hybbx_plugin_packet_radio) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_packet_radio + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/packet_radio/ax25.c b/plugins/packet_radio/ax25.c new file mode 100644 index 0000000..5cbcfd8 --- /dev/null +++ b/plugins/packet_radio/ax25.c @@ -0,0 +1,250 @@ +#include "hybbx/ax25.h" + +#include +#include +#include + +static void address_upper(char *call) +{ + size_t i; + + for (i = 0; call[i] != '\0'; i++) { + call[i] = (char)toupper((unsigned char)call[i]); + } +} + +hybbx_result_t hybbx_ax25_address_parse(const char *text, + hybbx_ax25_address_t *out) +{ + const char *dash; + size_t call_len; + char *end = NULL; + unsigned long ssid; + + if (text == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + dash = strchr(text, '-'); + if (dash != NULL) { + call_len = (size_t)(dash - text); + ssid = strtoul(dash + 1, &end, 10); + if (end == dash + 1 || ssid > HYBBX_AX25_SSID_MAX) { + return HYBBX_ERR_INVALID; + } + out->ssid = (unsigned)ssid; + } else { + call_len = strlen(text); + } + + if (call_len == 0 || call_len > HYBBX_AX25_CALL_MAX) { + return HYBBX_ERR_INVALID; + } + + memcpy(out->call, text, call_len); + out->call[call_len] = '\0'; + address_upper(out->call); + return HYBBX_OK; +} + +void hybbx_ax25_encode_address(uint8_t *out, const hybbx_ax25_address_t *addr, + int last) +{ + size_t i; + char padded[HYBBX_AX25_CALL_MAX]; + + memset(padded, ' ', sizeof(padded)); + for (i = 0; i < HYBBX_AX25_CALL_MAX && addr->call[i] != '\0'; i++) { + padded[i] = addr->call[i]; + } + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + out[i] = (uint8_t)((unsigned char)padded[i] << 1); + } + + out[6] = (uint8_t)(((addr->ssid & 0x0Fu) << 1) | 0x60u); + if (last) { + out[6] |= 0x01u; + } +} + +uint16_t hybbx_ax25_crc(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFFu; + size_t i; + int bit; + + for (i = 0; i < len; i++) { + crc ^= (uint16_t)data[i] << 8; + for (bit = 0; bit < 8; bit++) { + if (crc & 0x8000u) { + crc = (uint16_t)((crc << 1) ^ 0x1021u); + } else { + crc = (uint16_t)(crc << 1); + } + } + } + + return crc; +} + +static size_t append_addresses(const hybbx_ax25_path_t *path, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + unsigned i; + + if (out_cap < (size_t)(2 + path->digi_count) * HYBBX_AX25_ADDR_ENCODED) { + return 0; + } + + hybbx_ax25_encode_address(out + pos, &path->dest, 0); + pos += HYBBX_AX25_ADDR_ENCODED; + + for (i = 0; i < path->digi_count; i++) { + hybbx_ax25_encode_address(out + pos, &path->digi[i], 0); + pos += HYBBX_AX25_ADDR_ENCODED; + } + + hybbx_ax25_encode_address(out + pos, &path->source, 1); + pos += HYBBX_AX25_ADDR_ENCODED; + return pos; +} + +size_t hybbx_ax25_build_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos; + uint16_t crc; + + if (path == NULL || out == NULL || + (payload_len > 0 && payload == NULL)) { + return 0; + } + + if (payload_len + 32 > out_cap || payload_len > HYBBX_AX25_FRAME_MAX) { + return 0; + } + + pos = append_addresses(path, out, out_cap); + if (pos + 4 + payload_len > out_cap) { + return 0; + } + + out[pos++] = HYBBX_AX25_CTRL_UI; + out[pos++] = HYBBX_AX25_PID_NOL3; + + if (payload_len > 0) { + memcpy(out + pos, payload, payload_len); + pos += payload_len; + } + + crc = hybbx_ax25_crc(out, pos); + out[pos++] = (uint8_t)(crc & 0xFFu); + out[pos++] = (uint8_t)((crc >> 8) & 0xFFu); + return pos; +} + +static void decode_address(const uint8_t *in, hybbx_ax25_address_t *out) +{ + size_t i; + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + char ch = (char)((in[i] >> 1) & 0x7Fu); + out->call[i] = ch == ' ' ? '\0' : ch; + } + out->call[HYBBX_AX25_CALL_MAX] = '\0'; + + for (i = HYBBX_AX25_CALL_MAX; i > 0; i--) { + if (out->call[i - 1] != '\0') { + break; + } + } + out->call[i] = '\0'; + out->ssid = (unsigned)((in[6] >> 1) & 0x0Fu); +} + +size_t hybbx_ax25_parse_ui(const uint8_t *frame, size_t frame_len, + hybbx_ax25_path_t *path, + uint8_t *payload, size_t payload_cap) +{ + size_t pos = 0; + unsigned digi = 0; + uint16_t crc_rx; + uint16_t crc_calc; + + if (frame == NULL || frame_len < 16) { + return 0; + } + + if (path != NULL) { + memset(path, 0, sizeof(*path)); + } + + while (pos + HYBBX_AX25_ADDR_ENCODED <= frame_len) { + int last = frame[pos + 6] & 0x01; + hybbx_ax25_address_t tmp; + + decode_address(frame + pos, &tmp); + if (path != NULL) { + if (digi == 0) { + path->dest = tmp; + } else if (!last) { + if (digi - 1 < HYBBX_AX25_MAX_DIGI) { + path->digi[digi - 1] = tmp; + path->digi_count = digi; + } + } else { + path->source = tmp; + } + } + + pos += HYBBX_AX25_ADDR_ENCODED; + digi++; + + if (last) { + break; + } + + if (digi > HYBBX_AX25_MAX_DIGI + 1) { + return 0; + } + } + + if (pos + 4 > frame_len) { + return 0; + } + + if (frame[pos] != HYBBX_AX25_CTRL_UI) { + return 0; + } + + pos += 2; /* control + PID */ + + if (frame_len < pos + 2) { + return 0; + } + + crc_rx = (uint16_t)frame[frame_len - 2] | + ((uint16_t)frame[frame_len - 1] << 8); + crc_calc = hybbx_ax25_crc(frame, frame_len - 2); + if (crc_rx != crc_calc) { + return 0; + } + + { + size_t payload_len = frame_len - pos - 2; + + if (payload_len > payload_cap) { + return 0; + } + + if (payload_len > 0 && payload != NULL) { + memcpy(payload, frame + pos, payload_len); + } + + return payload_len; + } +} diff --git a/plugins/packet_radio/kiss.c b/plugins/packet_radio/kiss.c new file mode 100644 index 0000000..a075442 --- /dev/null +++ b/plugins/packet_radio/kiss.c @@ -0,0 +1,152 @@ +#include "hybbx/kiss.h" + +#include + +void hybbx_kiss_decoder_init(hybbx_kiss_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void deliver_frame(hybbx_kiss_decoder_t *dec, + hybbx_kiss_frame_cb cb, void *userdata) +{ + uint8_t port; + const uint8_t *payload; + size_t payload_len; + + if (dec->len < 1) { + return; + } + + port = (uint8_t)((dec->buf[0] >> 4) & 0x0Fu); + payload = dec->buf + 1; + payload_len = dec->len - 1; + + if (cb != NULL) { + cb(port, payload, payload_len, userdata); + } +} + +void hybbx_kiss_decoder_feed(hybbx_kiss_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_kiss_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + uint8_t byte = data[i]; + + if (byte == HYBBX_KISS_FEND) { + if (dec->in_frame && dec->len > 0) { + deliver_frame(dec, cb, userdata); + } + dec->in_frame = 1; + dec->escape = 0; + dec->len = 0; + continue; + } + + if (!dec->in_frame) { + continue; + } + + if (dec->escape) { + if (byte == HYBBX_KISS_TFEND) { + byte = HYBBX_KISS_FEND; + } else if (byte == HYBBX_KISS_TFESC) { + byte = HYBBX_KISS_FESC; + } + dec->escape = 0; + } else if (byte == HYBBX_KISS_FESC) { + dec->escape = 1; + continue; + } + + if (dec->len >= HYBBX_KISS_MAX_FRAME) { + dec->len = 0; + dec->in_frame = 0; + continue; + } + + dec->buf[dec->len++] = byte; + } +} + +static size_t kiss_write_byte(uint8_t byte, uint8_t *out, size_t out_cap, size_t pos) +{ + if (pos + 1 > out_cap) { + return 0; + } + + if (byte == HYBBX_KISS_FEND) { + if (pos + 2 > out_cap) { + return 0; + } + out[pos++] = HYBBX_KISS_FESC; + out[pos++] = HYBBX_KISS_TFEND; + return pos; + } + + if (byte == HYBBX_KISS_FESC) { + if (pos + 2 > out_cap) { + return 0; + } + out[pos++] = HYBBX_KISS_FESC; + out[pos++] = HYBBX_KISS_TFESC; + return pos; + } + + out[pos++] = byte; + return pos; +} + +size_t hybbx_kiss_encode(uint8_t port, hybbx_kiss_command_t cmd, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + uint8_t cmd_byte; + size_t i; + + if (out == NULL || out_cap < 3) { + return 0; + } + + if (payload_len > 0 && payload == NULL) { + return 0; + } + + if (payload_len + 4 > out_cap) { + return 0; + } + + out[pos++] = HYBBX_KISS_FEND; + + cmd_byte = (uint8_t)(((port & 0x0Fu) << 4) | ((uint8_t)cmd & 0x0Fu)); + pos = kiss_write_byte(cmd_byte, out, out_cap, pos); + if (pos == 0) { + return 0; + } + + for (i = 0; i < payload_len; i++) { + pos = kiss_write_byte(payload[i], out, out_cap, pos); + if (pos == 0) { + return 0; + } + } + + if (pos + 1 > out_cap) { + return 0; + } + + out[pos++] = HYBBX_KISS_FEND; + return pos; +} diff --git a/plugins/packet_radio/packet_radio.c b/plugins/packet_radio/packet_radio.c new file mode 100644 index 0000000..e8952ae --- /dev/null +++ b/plugins/packet_radio/packet_radio.c @@ -0,0 +1,1005 @@ +/* + * packet_radio — link/repeater edge adapter: serial TNC, KISS/AX.25 → HBX client. + * INI: [transport.packet_radio] or [transport.packet_radioN]. See share/hybbx.ini.example. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/broadcast.h" +#include "hybbx/packet_radio.h" +#include "hybbx/tnc.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/traffic.h" +#include "hybbx/limits.h" +#include "hybbx/security_ban.h" +#include "hybbx/ax25.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "hybbx/max25.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#include +#include +#endif + +#define PACKET_RADIO_POLL_MS 20u + +static hybbx_service_t *g_service; + +typedef struct packet_radio_instance { + hybbx_packet_radio_config_t config; + hybbx_tnc_t *tnc; + int circuit_fd; + hybbx_circuit_decoder_t circuit_dec; + volatile int flow_paused; + volatile int flow_cancel; + unsigned index; + unsigned circuit_reconnect_polls; +} packet_radio_instance_t; + +static packet_radio_instance_t g_instances[HYBBX_PACKET_RADIO_MAX_INSTANCES]; +static unsigned g_instance_count; +static pthread_t g_poll_thread; +static volatile int g_radio_running; +static int g_poll_thread_started; + +extern const hybbx_transport_plugin_t hybbx_plugin_packet_radio; + +static void instance_note_rf_activity(packet_radio_instance_t *inst); + +static unsigned packet_radio_count_local_edges(const char *config) +{ + char scratch[4096]; + const char *cursor = config; + unsigned count = 0; + + while (*cursor != '\0') { + const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP); + size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor); + + if (len == 0) { + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + if (len >= sizeof(scratch)) { + return count; + } + + memcpy(scratch, cursor, len); + scratch[len] = '\0'; + + if (hybbx_packet_radio_section_is_local_edge(scratch)) { + count++; + } + + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + } + + return count; +} + +static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst); +static void packet_radio_poll_sleep_ms(unsigned ms); + +static const char *device_type_name(hybbx_packet_radio_device_type_t type) +{ + return type == HYBBX_PACKET_RADIO_DEVICE_SERIAL ? "serial" : "usb"; +} + +static const char *protocol_name(hybbx_packet_radio_protocol_t proto) +{ + switch (proto) { + case HYBBX_PACKET_RADIO_PROTO_HOSTMODE: + return "hostmode"; + case HYBBX_PACKET_RADIO_PROTO_SIXPACK: + return "6pack"; + case HYBBX_PACKET_RADIO_PROTO_KISS: + default: + return "kiss"; + } +} + +static const char *tnc_name(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return "baycom"; + case HYBBX_PACKET_RADIO_TNC_PCCOM: + return "pccom"; + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return "generic"; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + return "tnc2c"; + } +} + +static int instance_circuit_uplink_allowed(const packet_radio_instance_t *inst) +{ + return inst != NULL && !inst->flow_paused && !inst->flow_cancel; +} + +static int packet_radio_source_banned(const hybbx_ax25_path_t *path) +{ + char raw[HYBBX_CALLID_MAX]; + char norm[HYBBX_CALLID_MAX]; + + if (path == NULL || path->source.call[0] == '\0') { + return 0; + } + + if (path->source.ssid > 0u) { + snprintf(raw, sizeof(raw), "%s-%u", path->source.call, + path->source.ssid); + } else { + hybbx_strlcpy(raw, path->source.call, sizeof(raw)); + } + + if (!hybbx_security_callid_normalize(raw, norm, sizeof(norm))) { + return 0; + } + + return hybbx_security_ban_callid_is_banned(norm); +} + +static int packet_radio_frame_source_banned(const uint8_t *frame, size_t len) +{ + hybbx_ax25_path_t path; + uint8_t scratch[HYBBX_AX25_FRAME_MAX]; + + if (frame == NULL || len == 0) { + return 0; + } + + if (hybbx_ax25_parse_ui(frame, len, &path, scratch, sizeof(scratch)) == 0) { + return 0; + } + + return packet_radio_source_banned(&path); +} + +static hybbx_result_t instance_circuit_uplink(packet_radio_instance_t *inst, + const uint8_t *frame, + size_t frame_len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + uint16_t flags = HYBBX_CIRCUIT_FLAG_RX; + + if (inst == NULL || inst->circuit_fd < 0) { + return HYBBX_ERR_IO; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return HYBBX_OK; + } + + if (hybbx_packet_radio_modulation_is_g3ruh(inst->config.params.modulation)) { + flags |= HYBBX_CIRCUIT_FLAG_G3RUH_FSK; + } + + hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, flags, + hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); +} + +static void on_tnc_ax25_frame(const uint8_t *frame, size_t len, void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + + if (len == 0 || inst == NULL) { + return; + } + + if (packet_radio_frame_source_banned(frame, len)) { + return; + } + + instance_note_rf_activity(inst); + (void)instance_circuit_uplink(inst, frame, len); +} + +static void on_tnc_host_ui(const uint8_t *payload, size_t len, + const hybbx_ax25_path_t *path, void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + (void)path; + + if (inst == NULL || inst->circuit_fd < 0 || len == 0) { + return; + } + + if (path != NULL && packet_radio_source_banned(path)) { + return; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return; + } + + hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_RX, + payload, len, hbx, sizeof(hbx)); + if (hbx_len > 0) { + (void)hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); + } +} + +static void instance_format_call(const hybbx_ax25_address_t *addr, + char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return; + } + + out[0] = '\0'; + if (addr == NULL || addr->call[0] == '\0') { + return; + } + + if (addr->ssid > 0u) { + unsigned ssid = addr->ssid; + + if (ssid > HYBBX_AX25_SSID_MAX) { + ssid = HYBBX_AX25_SSID_MAX; + } + snprintf(out, out_len, "%s-%u", addr->call, ssid); + } else { + hybbx_strlcpy(out, addr->call, out_len); + } +} + +static void instance_log_rf_tx(packet_radio_instance_t *inst, + hybbx_result_t rc, + const hybbx_ax25_path_t *path, + size_t payload_len) +{ + char src[HYBBX_AX25_CALL_MAX + 8]; + char dst[HYBBX_AX25_CALL_MAX + 8]; + + if (inst == NULL) { + return; + } + + instance_format_call(path != NULL ? &path->source : NULL, src, sizeof(src)); + instance_format_call(path != NULL ? &path->dest : NULL, dst, sizeof(dst)); + + if (rc == HYBBX_OK) { + hybbx_log_stats("[packet_radio%u] RF TX %s>%s (%zu bytes)", + inst->index + 1, src, dst, payload_len); + } else { + hybbx_log_warn("[packet_radio%u] RF TX failed %s>%s (%zu bytes, rc=%d)", + inst->index + 1, src, dst, payload_len, (int)rc); + } +} + +static void instance_note_rf_activity(packet_radio_instance_t *inst) +{ + hybbx_circuit_hub_t *hub; + const char *link_id; + + if (inst == NULL || g_service == NULL) { + return; + } + + hub = hybbx_service_circuit_hub(g_service); + link_id = inst->config.link_id; + if (link_id == NULL || link_id[0] == '\0') { + link_id = "packet-radio"; + } + + if (hub != NULL) { + hybbx_circuit_hub_note_rf_activity(hub, link_id); + } +} + +static hybbx_result_t instance_tx_ax25_ui(packet_radio_instance_t *inst, + const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_AX25_FRAME_MAX]; + size_t frame_len; + hybbx_result_t rc; + + if (inst == NULL || inst->tnc == NULL || path == NULL || + payload == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_ax25_build_ui(path, payload, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_tnc_send_frame(inst->tnc, frame, frame_len); + if (rc == HYBBX_OK) { + instance_note_rf_activity(inst); + } + instance_log_rf_tx(inst, rc, path, len); + return rc; +} + +static hybbx_result_t instance_send_bytes(packet_radio_instance_t *inst, + const uint8_t *data, size_t len) +{ + if (inst == NULL || inst->tnc == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + if (hybbx_tnc_host_connected(inst->tnc)) { + return hybbx_tnc_send_converse(inst->tnc, (const char *)data, len); + } + return HYBBX_ERR_BUSY; + } + + return hybbx_tnc_send_ui(inst->tnc, data, len); +} + +static void instance_circuit_disconnect(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + hybbx_circuit_decoder_init(&inst->circuit_dec); + inst->circuit_reconnect_polls = 0; +} + +static void instance_circuit_reconnect(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + instance_circuit_disconnect(inst); + packet_radio_poll_sleep_ms(250); + if (instance_connect_circuit(inst) == HYBBX_OK) { + hybbx_log_info("[packet_radio%u] circuit reconnected", inst->index + 1); + } +} + +static void instance_on_circuit_flow_ctrl(packet_radio_instance_t *inst, + hybbx_circuit_proto_t proto, + uint16_t flags, + const uint8_t *payload, size_t len) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + (void)proto; + (void)flags; + + if (inst == NULL) { + return; + } + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + inst->flow_paused = 1; + hybbx_log_stats("[packet_radio%u] circuit flow pause (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + inst->flow_paused = 0; + hybbx_circuit_decoder_init(&inst->circuit_dec); + hybbx_log_stats("[packet_radio%u] circuit flow break (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + inst->flow_cancel = 1; + hybbx_log_warn("[packet_radio%u] circuit flow cancel (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + inst->flow_paused = 0; + hybbx_log_stats("[packet_radio%u] circuit flow resume (%s)", + inst->index + 1, reason); + break; + default: + break; + } +} + +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + + if (inst == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + instance_on_circuit_flow_ctrl(inst, proto, flags, payload, len); + return; + } + + if (inst->tnc == NULL || len == 0) { + return; + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_TERMINAL: + (void)instance_send_bytes(inst, payload, len); + break; + case HYBBX_CIRCUIT_PROTO_AX25: { + hybbx_result_t rc = hybbx_tnc_send_frame(inst->tnc, payload, len); + if (rc == HYBBX_OK) { + hybbx_ax25_path_t path; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, + ui, sizeof(ui)); + + instance_note_rf_activity(inst); + if (ui_len > 0) { + instance_log_rf_tx(inst, rc, &path, ui_len); + } + } else { + hybbx_log_warn("[packet_radio%u] RF TX frame failed (rc=%d, %zu bytes)", + inst->index + 1, (int)rc, len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_AX25_UI: { + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, + ui, sizeof(ui)); + if (ui_len > 0) { + (void)instance_tx_ax25_ui(inst, &path, ui, ui_len); + } else { + hybbx_log_warn("[packet_radio%u] AX25_UI unpack failed (%zu bytes)", + inst->index + 1, len); + } + break; + } + default: + break; + } +} + +static void packet_radio_poll_sleep_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(ms / 1000u); + tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); +#endif +} + +static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst) +{ + const char *host; + unsigned port; + unsigned attempt; + + if (inst == NULL) { + return HYBBX_ERR_INVALID; + } + + host = inst->config.circuit_host; + port = inst->config.circuit_port; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + + if (inst->config.link_password == NULL || + inst->config.link_password[0] == '\0') { + hybbx_log_warn("[packet_radio%u] missing link_password for HBX LINK_AUTH; " + "set link_password in [transport.packet_radioN] " + "(hub logs link_auth_fail reason=timeout_no_link_auth)", + inst->index + 1); + return HYBBX_ERR_INVALID; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &inst->circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&inst->circuit_dec); + { + const char *link_id = inst->config.link_id; + const char *link_role = inst->config.link_role; + + if (link_id == NULL || link_id[0] == '\0') { + link_id = "packet-radio"; + } + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + { + hybbx_circuit_link_qos_t qos; + int duplex = (int)inst->config.params.duplex; + + memset(&qos, 0, sizeof(qos)); + qos.baud = inst->config.params.radio_baud; + if (duplex == HYBBX_PACKET_RADIO_DUPLEX_HALF) { + qos.duplex = 1; + } else if (duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) { + qos.duplex = 2; + } + qos.bandwidth = "low"; + qos.frequency_mhz = inst->config.frequency_mhz; + + rc = hybbx_circuit_link_authenticate_ex( + inst->circuit_fd, inst->config.link_password, + link_role, link_id, &qos); + } + if (rc != HYBBX_OK) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + packet_radio_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[packet_radio%u] linked to internal circuit %s:%u (HBX)", + inst->index + 1, host, port); + return HYBBX_OK; + } + packet_radio_poll_sleep_ms(100); + } + + hybbx_log_warn("[packet_radio%u] could not connect to internal circuit %s:%u", + inst->index + 1, host, port); + return HYBBX_ERR_IO; +} + +static void instance_shutdown(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + + if (inst->tnc != NULL) { + hybbx_tnc_close(inst->tnc); + inst->tnc = NULL; + } + + hybbx_packet_radio_config_free(&inst->config); + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; +} + +typedef enum { + PR_POLL_CIRCUIT = 0, + PR_POLL_SERIAL = 1 +} packet_radio_poll_kind_t; + +typedef struct { + unsigned instance_index; + packet_radio_poll_kind_t kind; +} packet_radio_poll_map_t; + +static void instance_drain_circuit(packet_radio_instance_t *inst) +{ + uint8_t buf[512]; + + if (inst == NULL || inst->circuit_fd < 0) { + return; + } + + for (;;) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(inst->circuit_fd, buf, + sizeof(buf), &read_len); + + if (rc == HYBBX_ERR_IO) { + hybbx_log_warn("[packet_radio%u] circuit disconnected — reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + return; + } + if (rc != HYBBX_OK || read_len == 0) { + break; + } + + hybbx_circuit_decoder_feed(&inst->circuit_dec, buf, read_len, + on_circuit_downlink, inst); + } +} + +#if !defined(_WIN32) +static void packet_radio_poll_instances(void) +{ + struct pollfd pfds[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u]; + packet_radio_poll_map_t map[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u]; + nfds_t nfds = 0; + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + int circuit_fd = inst->circuit_fd; + int serial_fd = inst->tnc != NULL ? hybbx_tnc_serial_fd(inst->tnc) : -1; + + if (circuit_fd >= 0) { + pfds[nfds].fd = circuit_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + map[nfds].instance_index = i; + map[nfds].kind = PR_POLL_CIRCUIT; + nfds++; + } + + if (serial_fd >= 0) { + pfds[nfds].fd = serial_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + map[nfds].instance_index = i; + map[nfds].kind = PR_POLL_SERIAL; + nfds++; + } + } + + if (nfds > 0) { + int pr = poll(pfds, nfds, (int)PACKET_RADIO_POLL_MS); + + if (pr < 0) { + if (errno != EINTR) { + hybbx_log_warn("[packet_radio] poll failed"); + } + } else if (pr > 0) { + nfds_t j; + + for (j = 0; j < nfds; j++) { + short rev = pfds[j].revents; + + if (rev == 0) { + continue; + } + + if ((rev & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + if (map[j].kind == PR_POLL_CIRCUIT) { + packet_radio_instance_t *inst = + &g_instances[map[j].instance_index]; + + hybbx_log_warn("[packet_radio%u] circuit hangup — " + "reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + } + continue; + } + + if ((rev & POLLIN) == 0) { + continue; + } + + if (map[j].kind == PR_POLL_CIRCUIT) { + instance_drain_circuit(&g_instances[map[j].instance_index]); + } else { + packet_radio_instance_t *inst = + &g_instances[map[j].instance_index]; + + if (inst->tnc != NULL) { + (void)hybbx_tnc_poll(inst->tnc); + } + } + } + } + } else { + packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS); + } +} +#endif + +static void packet_radio_service_maintenance(void) +{ + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + + if (inst->circuit_fd < 0) { + inst->circuit_reconnect_polls++; + if (inst->circuit_reconnect_polls >= 50u) { + inst->circuit_reconnect_polls = 0; + (void)instance_connect_circuit(inst); + } + } + + if (inst->flow_cancel) { + inst->flow_cancel = 0; + inst->flow_paused = 0; + instance_circuit_reconnect(inst); + } + } +} + +#if defined(_WIN32) +static void packet_radio_poll_win32(void) +{ + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + + if (inst->tnc != NULL) { + (void)hybbx_tnc_poll(inst->tnc); + } + + if (inst->circuit_fd >= 0) { + instance_drain_circuit(inst); + } + } +} +#endif + +static void *packet_radio_poll_thread(void *arg) +{ + (void)arg; + + while (g_radio_running) { +#if !defined(_WIN32) + packet_radio_poll_instances(); +#else + packet_radio_poll_win32(); + packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS); +#endif + packet_radio_service_maintenance(); + } + + return NULL; +} + +static hybbx_result_t instance_start(packet_radio_instance_t *inst, + const char *config, + unsigned index) +{ + hybbx_result_t rc; + hybbx_tnc_frame_cb frame_cb = on_tnc_ax25_frame; + hybbx_tnc_ui_cb ui_cb = NULL; + + if (inst == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; + inst->index = index; + + rc = hybbx_packet_radio_config_parse(config, &inst->config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[packet_radio%u] invalid configuration", index + 1); + instance_shutdown(inst); + return rc; + } + + if (g_service != NULL) { + const hybbx_broadcast_config_t *bc = + hybbx_service_get_broadcast(g_service); + + if (bc != NULL && bc->ax25_mycall[0] != '\0') { + size_t n = strlen(bc->ax25_mycall) + 1; + char *mc = malloc(n); + + if (mc != NULL) { + memcpy(mc, bc->ax25_mycall, n); + free(inst->config.mycall); + inst->config.mycall = mc; + hybbx_log_info("[packet_radio%u] ax25 source=%s (from [broadcast] ax25_mycall)", + index + 1, inst->config.mycall); + } + } + } + + hybbx_log_info("[packet_radio%u] tnc=%s protocol=%s device_type=%s device=%s " + "band=%s duplex=%s modulation=%s circuit=%s:%u " + "(HBX over internal TCP)", + index + 1, + tnc_name(inst->config.tnc), + protocol_name(inst->config.protocol), + device_type_name(inst->config.device_type), + inst->config.device, + hybbx_packet_radio_band_name(inst->config.params.band), + hybbx_packet_radio_duplex_name(inst->config.params.duplex), + hybbx_packet_radio_modulation_name(inst->config.params.modulation), + inst->config.circuit_host, + inst->config.circuit_port); + if (inst->config.frequency_mhz != NULL && + inst->config.frequency_mhz[0] != '\0') { + hybbx_log_info("[packet_radio%u] frequency_mhz=%s", index + 1, + inst->config.frequency_mhz); + } + + rc = instance_connect_circuit(inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + frame_cb = NULL; + ui_cb = on_tnc_host_ui; + } + + rc = hybbx_tnc_open(&inst->tnc, &inst->config, frame_cb, ui_cb, inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t packet_radio_init(hybbx_service_t *service) +{ + g_service = service; + g_instance_count = 0; + g_radio_running = 0; + return HYBBX_OK; +} + +static void packet_radio_shutdown(void) +{ + unsigned i; + + g_radio_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + for (i = 0; i < g_instance_count; i++) { + instance_shutdown(&g_instances[i]); + } + g_instance_count = 0; +} + +static hybbx_result_t packet_radio_start(const char *config) +{ + char scratch[4096]; + const char *cursor; + hybbx_max25_config_t max25; + hybbx_max25_status_t max25_status; + hybbx_result_t rc = HYBBX_OK; + unsigned started = 0; + unsigned section_num = 0; + unsigned local_edges; + + (void)g_service; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + packet_radio_shutdown(); + g_poll_thread = (pthread_t)0; + + cursor = hybbx_max25_config_skip_prefix(config, &max25); + local_edges = packet_radio_count_local_edges(cursor); + if (local_edges > 0u) { + rc = hybbx_max25_wait_ready(&max25); + if (rc != HYBBX_OK) { + return rc; + } + } + + while (*cursor != '\0' && g_instance_count < HYBBX_PACKET_RADIO_MAX_INSTANCES) { + const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP); + size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor); + + if (len == 0) { + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + if (len >= sizeof(scratch)) { + packet_radio_shutdown(); + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, cursor, len); + scratch[len] = '\0'; + section_num++; + + if (!hybbx_packet_radio_section_is_local_edge(scratch)) { + hybbx_log_info("[packet_radio%u] bridge registry only — no local TNC", + section_num); + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + rc = instance_start(&g_instances[g_instance_count], scratch, + g_instance_count); + if (rc != HYBBX_OK) { + packet_radio_shutdown(); + return rc; + } + + g_instance_count++; + started++; + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + } + + if (started == 0) { + return HYBBX_OK; + } + + g_radio_running = 1; + if (pthread_create(&g_poll_thread, NULL, packet_radio_poll_thread, + NULL) != 0) { + hybbx_log_warn("[packet_radio] poll thread failed"); + packet_radio_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + hybbx_log_info("[packet_radio] %u TNC instance(s) active", started); + return HYBBX_OK; +} + +static hybbx_result_t packet_radio_stop(void) +{ + hybbx_log_info("[packet_radio] stop"); + packet_radio_shutdown(); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_packet_radio = { + .name = "packet_radio", + .kind = HYBBX_TRANSPORT_PACKET_RADIO, + .version = 1, + .init = packet_radio_init, + .shutdown = packet_radio_shutdown, + .start = packet_radio_start, + .stop = packet_radio_stop, + .write = NULL, +}; diff --git a/plugins/packet_radio/packet_radio_config.c b/plugins/packet_radio/packet_radio_config.c new file mode 100644 index 0000000..f8e2d65 --- /dev/null +++ b/plugins/packet_radio/packet_radio_config.c @@ -0,0 +1,786 @@ +#include "hybbx/packet_radio.h" +#include "hybbx/tnc.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +int hybbx_packet_radio_section_is_local_edge(const char *config) +{ + char scratch[64]; + + if (config == NULL) { + return 0; + } + + if (find_kv(config, "circuit_host", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "device", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "tnc", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "protocol", scratch, sizeof(scratch)) != NULL) { + return 1; + } + + return 0; +} + +static hybbx_packet_radio_device_type_t parse_device_type(const char *value) +{ + if (value == NULL) { + return HYBBX_PACKET_RADIO_DEVICE_USB; + } + + if (str_ieq(value, "usb") || str_ieq(value, "ax25") || + str_ieq(value, "ax.25")) { + return HYBBX_PACKET_RADIO_DEVICE_USB; + } + + if (str_ieq(value, "serial") || str_ieq(value, "com") || + str_ieq(value, "rs232") || str_ieq(value, "v24")) { + return HYBBX_PACKET_RADIO_DEVICE_SERIAL; + } + + return HYBBX_PACKET_RADIO_DEVICE_USB; +} + +static const char *default_device_for_type(hybbx_packet_radio_device_type_t type) +{ + if (type == HYBBX_PACKET_RADIO_DEVICE_SERIAL) { + return HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL; + } + return HYBBX_PACKET_RADIO_DEFAULT_DEVICE_USB; +} + +static hybbx_packet_radio_tnc_t parse_tnc(const char *value) +{ + if (value == NULL || str_ieq(value, "tnc2c") || str_ieq(value, "tnc2-c")) { + return HYBBX_PACKET_RADIO_TNC_TNC2C; + } + + if (str_ieq(value, "tnc2") || str_ieq(value, "tnc-2") || + str_ieq(value, "pktnc2") || str_ieq(value, "pk-tnc2") || + str_ieq(value, "tapr") || str_ieq(value, "thefirmware") || + str_ieq(value, "tf")) { + return HYBBX_PACKET_RADIO_TNC_TNC2; + } + + if (str_ieq(value, "pk232") || str_ieq(value, "pk-232") || + str_ieq(value, "aea") || str_ieq(value, "pk87") || + str_ieq(value, "pk-87")) { + return HYBBX_PACKET_RADIO_TNC_PK232; + } + + if (str_ieq(value, "mfj1278") || str_ieq(value, "mfj-1278") || + str_ieq(value, "mfj1278b") || str_ieq(value, "mfj")) { + return HYBBX_PACKET_RADIO_TNC_MFJ1278; + } + + if (str_ieq(value, "kantronics") || str_ieq(value, "kpc") || + str_ieq(value, "kpc3") || str_ieq(value, "kpc9612")) { + return HYBBX_PACKET_RADIO_TNC_KANTRONICS; + } + + if (str_ieq(value, "baycom") || str_ieq(value, "bay-com")) { + return HYBBX_PACKET_RADIO_TNC_BAYCOM; + } + + if (str_ieq(value, "pccom") || str_ieq(value, "pc-com") || + str_ieq(value, "albrecht")) { + return HYBBX_PACKET_RADIO_TNC_PCCOM; + } + + if (str_ieq(value, "generic") || str_ieq(value, "tnc")) { + return HYBBX_PACKET_RADIO_TNC_GENERIC; + } + + return HYBBX_PACKET_RADIO_TNC_TNC2C; +} + +static hybbx_packet_radio_protocol_t parse_protocol(const char *value) +{ + if (value == NULL || str_ieq(value, "kiss")) { + return HYBBX_PACKET_RADIO_PROTO_KISS; + } + + if (str_ieq(value, "hostmode") || str_ieq(value, "host") || + str_ieq(value, "tnc2")) { + return HYBBX_PACKET_RADIO_PROTO_HOSTMODE; + } + + if (str_ieq(value, "6pack") || str_ieq(value, "sixpack")) { + return HYBBX_PACKET_RADIO_PROTO_SIXPACK; + } + + return HYBBX_PACKET_RADIO_PROTO_KISS; +} + +static hybbx_tnc2c_modem_t parse_modem(const char *value) +{ + if (value == NULL || str_ieq(value, "tcm3105") || str_ieq(value, "1200")) { + return HYBBX_TNC2C_MODEM_TCM3105; + } + + if (str_ieq(value, "9600")) { + return HYBBX_TNC2C_MODEM_9600; + } + + if (str_ieq(value, "g3ruh") || str_ieq(value, "g3ruh_fsk") || + str_ieq(value, "fsk9600")) { + return HYBBX_TNC2C_MODEM_9600; + } + + if (str_ieq(value, "19200")) { + return HYBBX_TNC2C_MODEM_19200; + } + + return HYBBX_TNC2C_MODEM_TCM3105; +} + +static float parse_clock_mhz(const char *value) +{ + if (value == NULL) { + return HYBBX_TNC2C_CLOCK_4_9_MHZ; + } + + if (str_ieq(value, "9.8") || str_ieq(value, "9,8")) { + return HYBBX_TNC2C_CLOCK_9_8_MHZ; + } + + if (str_ieq(value, "10") || str_ieq(value, "10.0")) { + return HYBBX_TNC2C_CLOCK_10_MHZ; + } + + return HYBBX_TNC2C_CLOCK_4_9_MHZ; +} + +static unsigned int parse_uint(const char *value, unsigned int default_value) +{ + char *end = NULL; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return default_value; + } + + n = strtoul(value, &end, 10); + if (end == value || n == 0) { + return default_value; + } + + return (unsigned int)n; +} + +static hybbx_packet_radio_band_t parse_radio_band(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_BAND_UNSET; + } + + if (str_ieq(value, "cb") || str_ieq(value, "11m") || + str_ieq(value, "27mhz") || str_ieq(value, "citizen")) { + return HYBBX_PACKET_RADIO_BAND_CB; + } + + if (str_ieq(value, "amateur") || str_ieq(value, "ham") || + str_ieq(value, "amateurfunk") || str_ieq(value, "afu")) { + return HYBBX_PACKET_RADIO_BAND_AMATEUR; + } + + return HYBBX_PACKET_RADIO_BAND_UNSET; +} + +static hybbx_packet_radio_duplex_t parse_radio_duplex(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_DUPLEX_UNSET; + } + + if (str_ieq(value, "full") || str_ieq(value, "full-duplex") || + str_ieq(value, "fullduplex") || str_ieq(value, "fulldup")) { + return HYBBX_PACKET_RADIO_DUPLEX_FULL; + } + + if (str_ieq(value, "half") || str_ieq(value, "half-duplex") || + str_ieq(value, "duplex") || str_ieq(value, "simplex")) { + return HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + return HYBBX_PACKET_RADIO_DUPLEX_UNSET; +} + +static hybbx_packet_radio_modulation_t parse_modulation(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_MOD_UNSET; + } + + if (str_ieq(value, "g3ruh") || str_ieq(value, "g3ruh_fsk") || + str_ieq(value, "fsk9600") || str_ieq(value, "9600fsk")) { + return HYBBX_PACKET_RADIO_MOD_G3RUH_FSK; + } + + if (str_ieq(value, "afsk") || str_ieq(value, "bell202") || + str_ieq(value, "tcm3105") || str_ieq(value, "1200")) { + return HYBBX_PACKET_RADIO_MOD_AFSK; + } + + return HYBBX_PACKET_RADIO_MOD_UNSET; +} + +static hybbx_tnc_serial_parity_t parse_serial_parity(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_TNC_SERIAL_PARITY_UNSET; + } + + if (str_ieq(value, "even") || str_ieq(value, "e")) { + return HYBBX_TNC_SERIAL_PARITY_EVEN; + } + + if (str_ieq(value, "odd") || str_ieq(value, "o")) { + return HYBBX_TNC_SERIAL_PARITY_ODD; + } + + if (str_ieq(value, "none") || str_ieq(value, "n") || str_ieq(value, "off")) { + return HYBBX_TNC_SERIAL_PARITY_NONE; + } + + return HYBBX_TNC_SERIAL_PARITY_UNSET; +} + +static void apply_serial_line(hybbx_packet_radio_config_t *out, const char *value) +{ + if (value == NULL || value[0] == '\0') { + return; + } + + if (str_ieq(value, "7e1")) { + out->params.data_bits = 7; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_EVEN; + out->params.stop_bits = 1; + return; + } + + if (str_ieq(value, "8n1")) { + out->params.data_bits = 8; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_NONE; + out->params.stop_bits = 1; + return; + } + + if (str_ieq(value, "7o1")) { + out->params.data_bits = 7; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_ODD; + out->params.stop_bits = 1; + } +} + +static hybbx_kiss_entry_t parse_kiss_entry(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_KISS_ENTRY_UNSET; + } + + if (str_ieq(value, "none") || str_ieq(value, "skip") || + str_ieq(value, "off") || str_ieq(value, "no")) { + return HYBBX_KISS_ENTRY_NONE; + } + + if (str_ieq(value, "kiss_on") || str_ieq(value, "kiss-on") || + str_ieq(value, "command") || str_ieq(value, "on") || + str_ieq(value, "yes")) { + return HYBBX_KISS_ENTRY_KISS_ON; + } + + if (str_ieq(value, "esc_at_k") || str_ieq(value, "esc@k") || + str_ieq(value, "esc") || str_ieq(value, "@k")) { + return HYBBX_KISS_ENTRY_ESC_AT_K; + } + + if (str_ieq(value, "auto")) { + return HYBBX_KISS_ENTRY_AUTO; + } + + return HYBBX_KISS_ENTRY_UNSET; +} + +static hybbx_kiss_exit_t parse_kiss_exit(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_KISS_EXIT_UNSET; + } + + if (str_ieq(value, "none") || str_ieq(value, "skip") || + str_ieq(value, "off") || str_ieq(value, "no")) { + return HYBBX_KISS_EXIT_NONE; + } + + if (str_ieq(value, "kiss_off") || str_ieq(value, "kiss-off") || + str_ieq(value, "command")) { + return HYBBX_KISS_EXIT_KISS_OFF; + } + + if (str_ieq(value, "kiss_frame") || str_ieq(value, "frame") || + str_ieq(value, "fend")) { + return HYBBX_KISS_EXIT_KISS_FRAME; + } + + if (str_ieq(value, "auto")) { + return HYBBX_KISS_EXIT_AUTO; + } + + return HYBBX_KISS_EXIT_UNSET; +} + +static unsigned int default_radio_baud_for_modem(hybbx_tnc2c_modem_t modem) +{ + switch (modem) { + case HYBBX_TNC2C_MODEM_9600: + return 9600; + case HYBBX_TNC2C_MODEM_19200: + return 19200; + case HYBBX_TNC2C_MODEM_TCM3105: + default: + return HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } +} + +static hybbx_result_t parse_via_list(hybbx_packet_radio_config_t *out, + const char *value) +{ + char scratch[256]; + char *cursor; + char *token; + + if (value == NULL || value[0] == '\0') { + return HYBBX_OK; + } + + if (strlen(value) >= sizeof(scratch)) { + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, value, strlen(value) + 1); + cursor = scratch; + + while ((token = strtok(cursor, ",")) != NULL && out->via_count < HYBBX_PACKET_RADIO_VIA_MAX) { + char *item = hybbx_strdup(token); + if (item == NULL) { + return HYBBX_ERR_NOMEM; + } + out->via[out->via_count++] = item; + cursor = NULL; + } + + return HYBBX_OK; +} + +int hybbx_packet_radio_baud_valid(unsigned int baud) +{ + return baud > 0; +} + +hybbx_result_t hybbx_packet_radio_config_parse(const char *config, + hybbx_packet_radio_config_t *out) +{ + char scratch[256]; + const char *value; + unsigned long baud; + hybbx_result_t rc; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + out->tnc = HYBBX_PACKET_RADIO_TNC_TNC2C; + out->protocol = 0; /* profile may choose */ + out->params.clock_mhz = HYBBX_TNC2C_CLOCK_4_9_MHZ; + out->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + out->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + out->params.kiss_port = 0; + out->params.txdelay = 50; + out->params.persist = 0; + out->params.slot = 10; + out->params.txtail = 0; + out->params.band = HYBBX_PACKET_RADIO_BAND_UNSET; + out->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_UNSET; + out->params.modulation = HYBBX_PACKET_RADIO_MOD_UNSET; + out->params.kiss_entry = HYBBX_KISS_ENTRY_UNSET; + out->params.kiss_exit = HYBBX_KISS_EXIT_UNSET; + out->params.host_connect_on_start = 0; + out->params.assert_modem_lines = -1; + + value = find_kv(config, "device_type", scratch, sizeof(scratch)); + out->device_type = parse_device_type(value); + + value = find_kv(config, "device", scratch, sizeof(scratch)); + if (value == NULL) { + out->device = hybbx_strdup(default_device_for_type(out->device_type)); + } else { + out->device = hybbx_strdup(value); + } + + if (out->device == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + + value = find_kv(config, "baud", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + out->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } else { + baud = strtoul(value, NULL, 10); + if (!hybbx_packet_radio_baud_valid((unsigned int)baud)) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + out->baud = (unsigned int)baud; + } + + value = find_kv(config, "tnc", scratch, sizeof(scratch)); + out->tnc = parse_tnc(value); + + if (out->tnc == HYBBX_PACKET_RADIO_TNC_BAYCOM || + out->tnc == HYBBX_PACKET_RADIO_TNC_PCCOM) { + hybbx_log_warn("[packet_radio] tnc=%s is not a packet_radio profile — " + "use tnc=tnc2c (or generic) with kiss_entry=none after " + "max25d prep; BayCom/based SER12 = MAX25 max25e0 (bcpr), " + "optional [networks] baycom=yes on non-UN1TME sites only", + hybbx_packet_radio_tnc_name(out->tnc)); + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + + value = find_kv(config, "protocol", scratch, sizeof(scratch)); + if (value != NULL) { + out->protocol = parse_protocol(value); + } + + value = find_kv(config, "clock_mhz", scratch, sizeof(scratch)); + out->params.clock_mhz = parse_clock_mhz(value); + + value = find_kv(config, "modem", scratch, sizeof(scratch)); + out->params.modem = parse_modem(value); + + value = find_kv(config, "radio_baud", scratch, sizeof(scratch)); + if (value == NULL) { + out->params.radio_baud = default_radio_baud_for_modem(out->params.modem); + } else { + unsigned int radio_baud = parse_uint(value, 0); + if (!hybbx_packet_radio_baud_valid(radio_baud)) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + out->params.radio_baud = radio_baud; + } + + value = find_kv(config, "kiss_port", scratch, sizeof(scratch)); + out->params.kiss_port = parse_uint(value, 0); + + value = find_kv(config, "txdelay", scratch, sizeof(scratch)); + out->params.txdelay = parse_uint(value, out->params.txdelay); + + value = find_kv(config, "persist", scratch, sizeof(scratch)); + out->params.persist = parse_uint(value, out->params.persist); + + value = find_kv(config, "slot", scratch, sizeof(scratch)); + out->params.slot = parse_uint(value, out->params.slot); + + value = find_kv(config, "txtail", scratch, sizeof(scratch)); + out->params.txtail = parse_uint(value, out->params.txtail); + + value = find_kv(config, "radio_band", scratch, sizeof(scratch)); + if (value != NULL) { + out->params.band = parse_radio_band(value); + } + + value = find_kv(config, "radio_duplex", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "duplex", scratch, sizeof(scratch)); + } + if (value != NULL) { + out->params.duplex = parse_radio_duplex(value); + } + + value = find_kv(config, "fullduplex", scratch, sizeof(scratch)); + if (value != NULL && + out->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + out->params.duplex = hybbx_parse_bool(value, 0) ? + HYBBX_PACKET_RADIO_DUPLEX_FULL : + HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + value = find_kv(config, "g3ruh_fsk", scratch, sizeof(scratch)); + if (value != NULL) { + out->params.modulation = hybbx_parse_bool(value, 0) ? + HYBBX_PACKET_RADIO_MOD_G3RUH_FSK : + HYBBX_PACKET_RADIO_MOD_AFSK; + } + + value = find_kv(config, "modulation", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_packet_radio_modulation_t mod = parse_modulation(value); + if (mod != HYBBX_PACKET_RADIO_MOD_UNSET) { + out->params.modulation = mod; + } + } + + value = find_kv(config, "kiss_entry", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_kiss_entry_t kiss_entry = parse_kiss_entry(value); + if (kiss_entry != HYBBX_KISS_ENTRY_UNSET) { + out->params.kiss_entry = kiss_entry; + } + } + + value = find_kv(config, "kiss_exit", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_kiss_exit_t kiss_exit = parse_kiss_exit(value); + if (kiss_exit != HYBBX_KISS_EXIT_UNSET) { + out->params.kiss_exit = kiss_exit; + } + } + + value = find_kv(config, "host_connect", scratch, sizeof(scratch)); + out->params.host_connect_on_start = hybbx_parse_bool(value, + out->params.host_connect_on_start); + + value = find_kv(config, "serial_line", scratch, sizeof(scratch)); + if (value != NULL) { + apply_serial_line(out, value); + } + + value = find_kv(config, "data_bits", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned int data_bits = parse_uint(value, 0); + if (data_bits == 7 || data_bits == 8) { + out->params.data_bits = data_bits; + } + } + + value = find_kv(config, "parity", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_tnc_serial_parity_t parity = parse_serial_parity(value); + if (parity != HYBBX_TNC_SERIAL_PARITY_UNSET) { + out->params.serial_parity = parity; + } + } + + value = find_kv(config, "stop_bits", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned int stop_bits = parse_uint(value, 0); + if (stop_bits == 1 || stop_bits == 2) { + out->params.stop_bits = stop_bits; + } + } + + value = find_kv(config, "assert_modem_lines", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "rts_dtr", scratch, sizeof(scratch)); + } + if (value != NULL) { + out->params.assert_modem_lines = hybbx_parse_bool(value, 0) ? 1 : 0; + } + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->mycall = hybbx_strdup(value); + if (out->mycall == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "dest", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "dest_call", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->dest_call = hybbx_strdup(value); + if (out->dest_call == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "via", scratch, sizeof(scratch)); + rc = parse_via_list(out, value); + if (rc != HYBBX_OK) { + hybbx_packet_radio_config_free(out); + return rc; + } + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->circuit_host = hybbx_strdup(value); + if (out->circuit_host == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "circuit_port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->circuit_port = parse_uint(value, HYBBX_CIRCUIT_DEFAULT_PORT); + } + + if (out->circuit_port == 0) { + out->circuit_port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + if (out->circuit_host == NULL) { + out->circuit_host = hybbx_strdup("127.0.0.1"); + if (out->circuit_host == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_id = hybbx_strdup(value); + if (out->link_id == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = hybbx_strdup(value); + if (out->link_password == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_role = hybbx_strdup(value); + if (out->link_role == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = hybbx_strdup(value); + if (out->frequency_mhz == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + (void)hybbx_tnc_profile_apply_defaults(out); + return HYBBX_OK; +} + +void hybbx_packet_radio_config_free(hybbx_packet_radio_config_t *config) +{ + unsigned i; + + if (config == NULL) { + return; + } + + free(config->device); + free(config->mycall); + free(config->dest_call); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + free(config->frequency_mhz); + for (i = 0; i < config->via_count; i++) { + free(config->via[i]); + config->via[i] = NULL; + } + + memset(config, 0, sizeof(*config)); +} diff --git a/plugins/packet_radio/serial_port.c b/plugins/packet_radio/serial_port.c new file mode 100644 index 0000000..70dd92c --- /dev/null +++ b/plugins/packet_radio/serial_port.c @@ -0,0 +1,705 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "serial_port.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include +#include +#include + +void hybbx_serial_params_default(hybbx_serial_params_t *params, + unsigned int baud) +{ + if (params == NULL) { + return; + } + + memset(params, 0, sizeof(*params)); + params->baud = baud; + params->data_bits = 8; + params->parity = HYBBX_SERIAL_PARITY_NONE; + params->stop_bits = 1; + params->assert_modem_lines = 0; +} + +#if defined(_WIN32) + +#define WIN32_LEAN_AND_MEAN +#include + +struct hybbx_serial_port { + HANDLE handle; + unsigned int baud; +}; + +static int win32_normalize_device(const char *device, char *out, size_t out_len) +{ + const char *name; + unsigned com_num = 0; + + if (device == NULL || out_len == 0) { + return 0; + } + + if (strncmp(device, "\\\\.\\", 4) == 0) { + return hybbx_strlcpy(out, device, out_len) < out_len; + } + + name = device; + if (name[0] == '/' && strncmp(name, "/dev/ttyS", 9) == 0 && + name[9] >= '0' && name[9] <= '9' && name[10] == '\0') { + com_num = (unsigned)(name[9] - '0') + 1u; + return (size_t)snprintf(out, out_len, "\\\\.\\COM%u", com_num) < out_len; + } + + if ((name[0] == 'C' || name[0] == 'c') && + (name[1] == 'O' || name[1] == 'o') && + (name[2] == 'M' || name[2] == 'm')) { + name += 3; + } + + if (*name == '\0') { + return 0; + } + + while (*name >= '0' && *name <= '9') { + com_num = com_num * 10u + (unsigned)(*name - '0'); + name++; + } + if (*name != '\0' || com_num == 0) { + return 0; + } + + return (size_t)snprintf(out, out_len, "\\\\.\\COM%u", com_num) < out_len; +} + +static hybbx_result_t win32_configure(HANDLE handle, + const hybbx_serial_params_t *params) +{ + DCB dcb; + COMMTIMEOUTS timeouts; + + memset(&dcb, 0, sizeof(dcb)); + dcb.DCBlength = sizeof(dcb); + if (!GetCommState(handle, &dcb)) { + return HYBBX_ERR_IO; + } + + dcb.BaudRate = (DWORD)params->baud; + dcb.ByteSize = (BYTE)(params->data_bits == 7 ? 7 : 8); + switch (params->parity) { + case HYBBX_SERIAL_PARITY_EVEN: + dcb.Parity = EVENPARITY; + dcb.fParity = TRUE; + break; + case HYBBX_SERIAL_PARITY_ODD: + dcb.Parity = ODDPARITY; + dcb.fParity = TRUE; + break; + case HYBBX_SERIAL_PARITY_NONE: + default: + dcb.Parity = NOPARITY; + dcb.fParity = FALSE; + break; + } + dcb.StopBits = params->stop_bits == 2 ? TWOSTOPBITS : ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = params->assert_modem_lines ? + DTR_CONTROL_ENABLE : DTR_CONTROL_DISABLE; + dcb.fRtsControl = params->assert_modem_lines ? + RTS_CONTROL_ENABLE : RTS_CONTROL_DISABLE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fNull = FALSE; + + if (!SetCommState(handle, &dcb)) { + return HYBBX_ERR_IO; + } + + memset(&timeouts, 0, sizeof(timeouts)); + timeouts.ReadIntervalTimeout = MAXDWORD; + timeouts.ReadTotalTimeoutConstant = 0; + timeouts.ReadTotalTimeoutMultiplier = 0; + timeouts.WriteTotalTimeoutConstant = 0; + timeouts.WriteTotalTimeoutMultiplier = 0; + if (!SetCommTimeouts(handle, &timeouts)) { + return HYBBX_ERR_IO; + } + + PurgeComm(handle, PURGE_RXCLEAR | PURGE_TXCLEAR); + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + char path[64]; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + if (!win32_normalize_device(device, path, sizeof(path))) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + port->handle = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (port->handle == INVALID_HANDLE_VALUE) { + free(port); + return HYBBX_ERR_IO; + } + + rc = win32_configure(port->handle, params); + if (rc != HYBBX_OK) { + CloseHandle(port->handle); + free(port); + return rc; + } + + port->baud = params->baud; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->handle != INVALID_HANDLE_VALUE) { + CloseHandle(port->handle); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + size_t written = 0; + + if (port == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + while (written < len) { + DWORD chunk = 0; + + if (!WriteFile(port->handle, data + written, + (DWORD)(len - written), &chunk, NULL)) { + return HYBBX_ERR_IO; + } + if (chunk == 0) { + return HYBBX_ERR_IO; + } + written += (size_t)chunk; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + DWORD n = 0; + + if (port == NULL || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!ReadFile(port->handle, buf, (DWORD)buf_len, &n, NULL)) { + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + (void)port; + return -1; +} + +#elif defined(__AMIGA__) + +#include +#include + +struct hybbx_serial_port { + struct IOExtSer *io; + struct MsgPort *port; + char device_name[32]; + unsigned long unit; + unsigned int baud; +}; + +static int amiga_parse_device(const char *device, char *name, size_t name_len, + unsigned long *unit) +{ + const char *colon; + char *end; + + if (unit == NULL || name == NULL || name_len == 0) { + return 0; + } + + *unit = 0; + if (device == NULL || device[0] == '\0') { + hybbx_strlcpy(name, SERIALNAME, name_len); + return 1; + } + + colon = strchr(device, ':'); + if (colon != NULL) { + size_t prefix = (size_t)(colon - device); + + if (prefix == 0 || prefix >= name_len) { + return 0; + } + memcpy(name, device, prefix); + name[prefix] = '\0'; + *unit = strtoul(colon + 1, &end, 10); + return end != colon + 1 && *end == '\0'; + } + + if (device[0] >= '0' && device[0] <= '9') { + *unit = strtoul(device, &end, 10); + if (end != device && *end == '\0') { + hybbx_strlcpy(name, SERIALNAME, name_len); + return 1; + } + } + + hybbx_strlcpy(name, device, name_len); + return 1; +} + +static hybbx_result_t amiga_configure(struct IOExtSer *io, + const hybbx_serial_params_t *params) +{ + io->IOSer.io_Command = SDCMD_SETPARAMS; + io->io_Baud = (ULONG)params->baud; + io->io_ReadLen = (UBYTE)(params->data_bits == 7 ? 7 : 8); + io->io_WriteLen = (UBYTE)(params->data_bits == 7 ? 7 : 8); + io->io_StopBits = (UBYTE)(params->stop_bits == 2 ? 2 : 1); + io->io_SerFlags = (UBYTE)(io->io_SerFlags & (UBYTE)~SERF_PARTY_ON); + if (params->parity == HYBBX_SERIAL_PARITY_EVEN || + params->parity == HYBBX_SERIAL_PARITY_ODD) { + io->io_SerFlags |= SERF_PARTY_ON; + } + io->io_SerFlags |= SERF_XDISABLED; + + if (DoIO((struct IORequest *)io) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + if (!amiga_parse_device(device, port->device_name, sizeof(port->device_name), + &port->unit)) { + free(port); + return HYBBX_ERR_INVALID; + } + + port->port = CreatePort(NULL, 0); + if (port->port == NULL) { + free(port); + return HYBBX_ERR_NOMEM; + } + + port->io = (struct IOExtSer *)CreateExtIO(port->port, + (LONG)sizeof(*port->io)); + if (port->io == NULL) { + DeletePort(port->port); + free(port); + return HYBBX_ERR_NOMEM; + } + + port->io->io_SerFlags = SERF_SHARED | SERF_XDISABLED; + if (OpenDevice(port->device_name, port->unit, + (struct IORequest *)port->io, 0) != 0) { + DeleteExtIO((struct IORequest *)port->io); + DeletePort(port->port); + free(port); + return HYBBX_ERR_IO; + } + + rc = amiga_configure(port->io, params); + if (rc != HYBBX_OK) { + CloseDevice((struct IORequest *)port->io); + DeleteExtIO((struct IORequest *)port->io); + DeletePort(port->port); + free(port); + return rc; + } + + port->baud = params->baud; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->io != NULL) { + CloseDevice((struct IORequest *)port->io); + DeleteExtIO((struct IORequest *)port->io); + } + if (port->port != NULL) { + DeletePort(port->port); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + if (port == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + port->io->IOSer.io_Command = CMD_WRITE; + port->io->IOSer.io_Data = (APTR)data; + port->io->IOSer.io_Length = (ULONG)len; + port->io->IOSer.io_Flags = 0; + + if (DoIO((struct IORequest *)port->io) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + if (port == NULL || buf == NULL || read_len == NULL || buf_len == 0) { + return HYBBX_ERR_INVALID; + } + + port->io->IOSer.io_Command = CMD_READ; + port->io->IOSer.io_Data = (APTR)buf; + port->io->IOSer.io_Length = (ULONG)buf_len; + port->io->IOSer.io_Flags = IOF_QUICK; + + if (DoIO((struct IORequest *)port->io) != 0) { + *read_len = 0; + return HYBBX_OK; + } + + *read_len = (size_t)port->io->IOSer.io_Actual; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + (void)port; + return -1; +} + +#else /* POSIX */ + +#include +#include +#include +#include +#include +#include + +struct hybbx_serial_port { + int fd; + unsigned int baud; + int assert_modem_lines; +}; + +#ifndef cfmakeraw +static void hybbx_cfmakeraw(struct termios *tty) +{ + tty->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | + IGNCR | ICRNL | IXON); + tty->c_oflag &= ~OPOST; + tty->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tty->c_cflag &= ~(CSIZE | PARENB); + tty->c_cflag |= CS8; +} +#define cfmakeraw hybbx_cfmakeraw +#endif + +static hybbx_result_t set_baud_rate(struct termios *tty, unsigned int baud) +{ +#if defined(__linux__) || defined(__GLIBC__) + if (cfsetspeed(tty, (speed_t)baud) == 0) { + return HYBBX_OK; + } +#endif + + { + speed_t speed = B0; + switch (baud) { + case 50: speed = B50; break; + case 75: speed = B75; break; + case 110: speed = B110; break; + case 134: speed = B134; break; + case 150: speed = B150; break; + case 200: speed = B200; break; + case 300: speed = B300; break; + case 600: speed = B600; break; + case 1200: speed = B1200; break; + case 1800: speed = B1800; break; + case 2400: speed = B2400; break; + case 4800: speed = B4800; break; + case 9600: speed = B9600; break; +#ifdef B19200 + case 19200: speed = B19200; break; +#endif +#ifdef B38400 + case 38400: speed = B38400; break; +#endif +#ifdef B57600 + case 57600: speed = B57600; break; +#endif +#ifdef B115200 + case 115200: speed = B115200; break; +#endif + default: + return HYBBX_ERR_UNSUPPORTED; + } + + cfsetispeed(tty, speed); + cfsetospeed(tty, speed); + } + + return HYBBX_OK; +} + +static hybbx_result_t posix_assert_modem_lines(int fd, int on) +{ +#ifdef TIOCMGET + int flags; + + /* BayCom KISS PTY (max25-bcpr): assert_modem_lines=0 — no UART modem bits. + * TIOCMGET on /dev/pts/* often fails; skip when not asserting. */ + if (!on) { + return HYBBX_OK; + } + + if (ioctl(fd, TIOCMGET, &flags) != 0) { + return HYBBX_ERR_IO; + } + flags |= TIOCM_RTS | TIOCM_DTR; + if (ioctl(fd, TIOCMSET, &flags) != 0) { + return HYBBX_ERR_IO; + } +#else + (void)fd; + (void)on; +#endif + return HYBBX_OK; +} + +static hybbx_result_t configure_port(int fd, const hybbx_serial_params_t *params) +{ + struct termios tty; + tcflag_t databits; + + if (tcgetattr(fd, &tty) != 0) { + return HYBBX_ERR_IO; + } + + cfmakeraw(&tty); + tty.c_cflag |= (CLOCAL | CREAD); + tty.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB); + if (params->data_bits == 7) { + databits = CS7; + } else { + databits = CS8; + } + tty.c_cflag |= databits; + if (params->parity == HYBBX_SERIAL_PARITY_EVEN) { + tty.c_cflag |= PARENB; + } else if (params->parity == HYBBX_SERIAL_PARITY_ODD) { + tty.c_cflag |= PARENB | PARODD; + } + if (params->stop_bits == 2) { + tty.c_cflag |= CSTOPB; + } + tty.c_cc[VMIN] = 0; + tty.c_cc[VTIME] = 1; + + if (set_baud_rate(&tty, params->baud) != HYBBX_OK) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (tcsetattr(fd, TCSANOW, &tty) != 0) { + return HYBBX_ERR_IO; + } + + tcflush(fd, TCIOFLUSH); + return posix_assert_modem_lines(fd, params->assert_modem_lines); +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + port->fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (port->fd < 0) { + hybbx_log_warn("[serial] open %s failed: %s", device, strerror(errno)); + free(port); + return HYBBX_ERR_IO; + } + + rc = configure_port(port->fd, params); + if (rc != HYBBX_OK) { + close(port->fd); + free(port); + return rc; + } + + port->baud = params->baud; + port->assert_modem_lines = params->assert_modem_lines; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->fd >= 0) { + close(port->fd); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + size_t written = 0; + + if (port == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (port->assert_modem_lines) { + hybbx_result_t rc = posix_assert_modem_lines(port->fd, 1); + + if (rc != HYBBX_OK) { + return rc; + } + } + + while (written < len) { + ssize_t n = write(port->fd, data + written, len - written); + + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + written += (size_t)n; + } + + if (tcdrain(port->fd) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + ssize_t n; + + if (port == NULL || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + n = read(port->fd, buf, buf_len); + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + *read_len = 0; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + if (port == NULL) { + return -1; + } + + return port->fd; +} + +#endif diff --git a/plugins/packet_radio/serial_port.h b/plugins/packet_radio/serial_port.h new file mode 100644 index 0000000..549d3c5 --- /dev/null +++ b/plugins/packet_radio/serial_port.h @@ -0,0 +1,52 @@ +#ifndef HYBBX_SERIAL_PORT_H +#define HYBBX_SERIAL_PORT_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_serial_port hybbx_serial_port_t; + +typedef enum hybbx_serial_parity { + HYBBX_SERIAL_PARITY_NONE = 0, + HYBBX_SERIAL_PARITY_EVEN = 1, + HYBBX_SERIAL_PARITY_ODD = 2 +} hybbx_serial_parity_t; + +typedef struct hybbx_serial_params { + unsigned int baud; + unsigned int data_bits; + hybbx_serial_parity_t parity; + unsigned int stop_bits; + int assert_modem_lines; +} hybbx_serial_params_t; + +void hybbx_serial_params_default(hybbx_serial_params_t *params, + unsigned int baud); + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params); + +void hybbx_serial_close(hybbx_serial_port_t *port); + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len); + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len); + +/** POSIX serial fd for poll/select; returns -1 when unavailable. */ +int hybbx_serial_fd(const hybbx_serial_port_t *port); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SERIAL_PORT_H */ diff --git a/plugins/packet_radio/sixpack.c b/plugins/packet_radio/sixpack.c new file mode 100644 index 0000000..28c0510 --- /dev/null +++ b/plugins/packet_radio/sixpack.c @@ -0,0 +1,146 @@ +#include "sixpack.h" + +#include + +void hybbx_sixpack_decoder_init(hybbx_sixpack_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void sixpack_reset(hybbx_sixpack_decoder_t *dec) +{ + dec->len = 0; + dec->count = 0; + dec->checksum = 0; + dec->in_frame = 0; +} + +static void sixpack_deliver(hybbx_sixpack_decoder_t *dec, + hybbx_sixpack_frame_cb cb, void *userdata) +{ + if (cb != NULL && dec->len > 0) { + cb(dec->buf, dec->len, userdata); + } + + sixpack_reset(dec); +} + +void hybbx_sixpack_decoder_feed(hybbx_sixpack_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_sixpack_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + uint8_t raw = data[i]; + uint8_t byte = raw; + + if (!dec->in_frame) { + if (raw == HYBBX_SIXPACK_LEAD) { + dec->in_frame = 1; + dec->len = 0; + dec->count = 0; + dec->checksum = 0; + } + continue; + } + + if (dec->count == 0) { + dec->count = raw; + dec->checksum = raw; + continue; + } + + dec->count--; + dec->checksum = (uint8_t)(dec->checksum + raw); + + if (raw < 0x30u) { + byte = (uint8_t)(raw + 0x40u); + } + + if (dec->count > 0) { + if (dec->len >= HYBBX_KISS_MAX_FRAME) { + sixpack_reset(dec); + continue; + } + dec->buf[dec->len++] = byte; + continue; + } + + if (dec->checksum == 0) { + sixpack_deliver(dec, cb, userdata); + } else { + sixpack_reset(dec); + } + } +} + +static size_t sixpack_write_encoded(uint8_t byte, uint8_t *out, size_t out_cap, + size_t pos) +{ + if (byte < 0x30u) { + if (pos + 1 > out_cap) { + return 0; + } + out[pos++] = (uint8_t)(byte + 0x40u); + return pos; + } + + if (pos + 1 > out_cap) { + return 0; + } + + out[pos++] = byte; + return pos; +} + +size_t hybbx_sixpack_encode(const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap) +{ + uint8_t scratch[HYBBX_KISS_MAX_FRAME + 4]; + size_t enc_len = 0; + size_t i; + uint8_t count; + uint8_t sum; + size_t pos = 0; + + if (frame == NULL || out == NULL || frame_len == 0 || + frame_len > HYBBX_KISS_MAX_FRAME) { + return 0; + } + + for (i = 0; i < frame_len; i++) { + enc_len = sixpack_write_encoded(frame[i], scratch, sizeof(scratch), + enc_len); + if (enc_len == 0) { + return 0; + } + } + + if (enc_len + 4 > out_cap || enc_len + 1 > 255) { + return 0; + } + + count = (uint8_t)(enc_len + 1); + sum = count; + for (i = 0; i < enc_len; i++) { + sum = (uint8_t)(sum + scratch[i]); + } + + out[pos++] = HYBBX_SIXPACK_LEAD; + out[pos++] = count; + for (i = 0; i < enc_len; i++) { + out[pos++] = scratch[i]; + } + out[pos++] = (uint8_t)(0x100u - (unsigned)sum); + + return pos; +} diff --git a/plugins/packet_radio/sixpack.h b/plugins/packet_radio/sixpack.h new file mode 100644 index 0000000..0d60e87 --- /dev/null +++ b/plugins/packet_radio/sixpack.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_SIXPACK_H +#define HYBBX_SIXPACK_H + +#include "hybbx/kiss.h" +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_SIXPACK_LEAD 0x53u /* 'S' */ + +typedef void (*hybbx_sixpack_frame_cb)(const uint8_t *frame, size_t len, + void *userdata); + +typedef struct hybbx_sixpack_decoder { + uint8_t buf[HYBBX_KISS_MAX_FRAME]; + size_t len; + int in_frame; + uint8_t count; + uint8_t checksum; +} hybbx_sixpack_decoder_t; + +void hybbx_sixpack_decoder_init(hybbx_sixpack_decoder_t *dec); + +void hybbx_sixpack_decoder_feed(hybbx_sixpack_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_sixpack_frame_cb cb, void *userdata); + +size_t hybbx_sixpack_encode(const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SIXPACK_H */ diff --git a/plugins/packet_radio/tnc.c b/plugins/packet_radio/tnc.c new file mode 100644 index 0000000..a42480d --- /dev/null +++ b/plugins/packet_radio/tnc.c @@ -0,0 +1,1080 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/tnc.h" +#include "hybbx/kiss.h" +#include "hybbx/rf_tx_pace.h" +#include "hybbx/traffic.h" +#include "serial_port.h" +#include "sixpack.h" +#include "tnc_host.h" + +#include "hybbx/log.h" + +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#endif + +struct hybbx_tnc { + hybbx_packet_radio_config_t config; + hybbx_serial_port_t *serial; + hybbx_kiss_decoder_t kiss_dec; + hybbx_sixpack_decoder_t sixpack_dec; + hybbx_tnc_host_t host; + hybbx_ax25_path_t tx_path; + int kiss_active; + hybbx_kiss_entry_t kiss_entry_used; + int host_ready; + hybbx_tnc_frame_cb frame_cb; + hybbx_tnc_ui_cb ui_cb; + void *rx_userdata; + unsigned long long last_rx_ms; +}; + +static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc); + +static void config_copy(hybbx_packet_radio_config_t *dst, + const hybbx_packet_radio_config_t *src) +{ + unsigned i; + + memset(dst, 0, sizeof(*dst)); + dst->device_type = src->device_type; + dst->device = src->device; + dst->baud = src->baud; + dst->tnc = src->tnc; + dst->protocol = src->protocol; + dst->params = src->params; + dst->mycall = src->mycall; + dst->dest_call = src->dest_call; + dst->via_count = src->via_count; + for (i = 0; i < src->via_count; i++) { + dst->via[i] = src->via[i]; + } +} + +static unsigned long long tnc_monotonic_ms(void) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + return (unsigned long long)ts.tv_sec * 1000ull + + (unsigned long long)ts.tv_nsec / 1000000ull; + } +#endif + return (unsigned long long)time(NULL) * 1000ull; +} + +static void tnc_note_rx(hybbx_tnc_t *tnc) +{ + if (tnc != NULL) { + tnc->last_rx_ms = tnc_monotonic_ms(); + } +} + +static hybbx_result_t tnc_wait_half_duplex_clear(hybbx_tnc_t *tnc) +{ + unsigned guard_ms; + unsigned long long now; + unsigned long long elapsed; + + if (tnc == NULL || hybbx_tnc_duplex_is_full(tnc)) { + return HYBBX_OK; + } + + guard_ms = tnc->config.params.slot * 10u; + if (guard_ms < 50u) { + guard_ms = 50u; + } + + now = tnc_monotonic_ms(); + if (tnc->last_rx_ms == 0 || now <= tnc->last_rx_ms) { + return HYBBX_OK; + } + + elapsed = now - tnc->last_rx_ms; + if (elapsed >= guard_ms) { + return HYBBX_OK; + } + +#if defined(_WIN32) + Sleep((DWORD)(guard_ms - elapsed)); +#else + { + struct timeval tv; + + tv.tv_sec = (time_t)((guard_ms - elapsed) / 1000u); + tv.tv_usec = (suseconds_t)(((guard_ms - elapsed) % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); + } +#endif + + return HYBBX_OK; +} + +static hybbx_result_t send_raw(hybbx_tnc_t *tnc, const uint8_t *data, size_t len) +{ + return hybbx_serial_write(tnc->serial, data, len); +} + +static hybbx_result_t send_cmd(hybbx_tnc_t *tnc, const char *cmd) +{ + char line[128]; + size_t len; + + len = hybbx_tnc_host_format_cmd(cmd, line, sizeof(line)); + if (len == 0) { + return HYBBX_ERR_INVALID; + } + + return send_raw(tnc, (const uint8_t *)line, len); +} + +static hybbx_result_t drain_serial(hybbx_tnc_t *tnc, unsigned rounds) +{ + uint8_t buf[128]; + size_t read_len; + unsigned i; + + for (i = 0; i < rounds; i++) { + if (hybbx_serial_read(tnc->serial, buf, sizeof(buf), + &read_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (read_len == 0) { + break; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t send_kiss_param(hybbx_tnc_t *tnc, + hybbx_kiss_command_t cmd, + uint8_t value) +{ + uint8_t frame[16]; + size_t len; + + len = hybbx_kiss_encode((uint8_t)tnc->config.params.kiss_port, cmd, + &value, 1, frame, sizeof(frame)); + if (len == 0) { + return HYBBX_ERR_IO; + } + + return send_raw(tnc, frame, len); +} + +static hybbx_result_t tnc_apply_kiss_params(hybbx_tnc_t *tnc) +{ + hybbx_result_t rc; + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_TXDELAY, + (uint8_t)tnc->config.params.txdelay); + if (rc != HYBBX_OK) { + return rc; + } + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_PERSIST, + (uint8_t)tnc->config.params.persist); + if (rc != HYBBX_OK) { + return rc; + } + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_SLOTTIME, + (uint8_t)tnc->config.params.slot); + if (rc != HYBBX_OK) { + return rc; + } + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_TXTAIL, + (uint8_t)tnc->config.params.txtail); + if (rc != HYBBX_OK) { + return rc; + } + + return send_kiss_param(tnc, HYBBX_KISS_CMD_FULLDUPLEX, + (uint8_t)(tnc->config.params.fullduplex ? 1 : 0)); +} + +static hybbx_result_t tnc_send_esc_at_k(hybbx_tnc_t *tnc) +{ + const uint8_t seq[] = { 0x1b, '@', 'K' }; + + return send_raw(tnc, seq, sizeof(seq)); +} + +static hybbx_result_t tnc_send_kiss_return_frame(hybbx_tnc_t *tnc) +{ + const uint8_t frame[] = { HYBBX_KISS_FEND, 0xFFu, HYBBX_KISS_FEND }; + + return send_raw(tnc, frame, sizeof(frame)); +} + +static void tnc_serial_pause_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timespec ts; + + ts.tv_sec = (time_t)(ms / 1000u); + ts.tv_nsec = (long)((ms % 1000u) * 1000000u); + (void)nanosleep(&ts, NULL); +#endif +} + +static hybbx_result_t tnc_send_jhost0(hybbx_tnc_t *tnc) +{ + static const uint8_t nuls[300] = { 0 }; + static const uint8_t jhost0[] = { + 0x00, 0x01, 0x06, 'J', 'H', 'O', 'S', 'T', ' ', '0', '\r' + }; + hybbx_result_t rc; + + rc = send_raw(tnc, nuls, sizeof(nuls)); + if (rc != HYBBX_OK) { + return rc; + } + tnc_serial_pause_ms(150); + return send_raw(tnc, jhost0, sizeof(jhost0)); +} + +static hybbx_result_t tnc_send_restart_escape(hybbx_tnc_t *tnc) +{ + unsigned i; + + for (i = 0; i < 3u; i++) { + if (send_raw(tnc, (const uint8_t *)"\x03", 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + tnc_serial_pause_ms(80); + } + tnc_serial_pause_ms(300); + return send_cmd(tnc, "RESTART"); +} + +/** Leave KISS/host/transparency → terminal (TheFirmware ladder). */ +static hybbx_result_t tnc_recover_terminal_mode(hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + tnc->kiss_active = 0; + (void)tnc_send_kiss_return_frame(tnc); + tnc_serial_pause_ms(400); + drain_serial(tnc, 8); + + (void)tnc_send_jhost0(tnc); + tnc_serial_pause_ms(800); + drain_serial(tnc, 8); + + (void)send_cmd(tnc, "kiss off"); + tnc_serial_pause_ms(400); + drain_serial(tnc, 6); + (void)send_cmd(tnc, "INFO"); + drain_serial(tnc, 12); + + (void)tnc_send_restart_escape(tnc); + tnc_serial_pause_ms(1200); + drain_serial(tnc, 8); + (void)send_cmd(tnc, "kiss off"); + tnc_serial_pause_ms(300); + drain_serial(tnc, 4); + + return HYBBX_OK; +} + +/** Leave KISS (crash/kill) so host commands (MYCALL, kiss on) work again. */ +static hybbx_result_t tnc_kiss_wakeup_host(hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + return tnc_recover_terminal_mode(tnc); +} + +static hybbx_result_t tnc_enter_kiss_by_entry(hybbx_tnc_t *tnc, + hybbx_kiss_entry_t entry) +{ + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (entry) { + case HYBBX_KISS_ENTRY_NONE: + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_NONE; + return HYBBX_OK; + + case HYBBX_KISS_ENTRY_ESC_AT_K: + if (tnc_send_esc_at_k(tnc) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_ESC_AT_K; + return HYBBX_OK; + + case HYBBX_KISS_ENTRY_KISS_ON: + if (send_cmd(tnc, "kiss on") != HYBBX_OK) { + return HYBBX_ERR_IO; + } + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_KISS_ON; + return HYBBX_OK; + + case HYBBX_KISS_ENTRY_AUTO: + if (send_cmd(tnc, "kiss on") == HYBBX_OK) { + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_KISS_ON; + return HYBBX_OK; + } + if (tnc_send_esc_at_k(tnc) == HYBBX_OK) { + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_ESC_AT_K; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + + case HYBBX_KISS_ENTRY_UNSET: + default: + return HYBBX_ERR_INVALID; + } +} + +static void tnc_exit_kiss_mode(hybbx_tnc_t *tnc) +{ + hybbx_kiss_exit_t exit_mode; + + if (tnc == NULL || !tnc->kiss_active || + tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) { + return; + } + + exit_mode = tnc->config.params.kiss_exit; + if (exit_mode == HYBBX_KISS_EXIT_UNSET) { + exit_mode = HYBBX_KISS_EXIT_AUTO; + } + if (exit_mode == HYBBX_KISS_EXIT_AUTO) { + if (tnc->kiss_entry_used == HYBBX_KISS_ENTRY_ESC_AT_K) { + exit_mode = HYBBX_KISS_EXIT_KISS_FRAME; + } else { + exit_mode = HYBBX_KISS_EXIT_KISS_OFF; + } + } + + switch (exit_mode) { + case HYBBX_KISS_EXIT_KISS_FRAME: + (void)tnc_send_kiss_return_frame(tnc); + break; + case HYBBX_KISS_EXIT_KISS_OFF: + (void)send_cmd(tnc, "kiss off"); + break; + case HYBBX_KISS_EXIT_NONE: + case HYBBX_KISS_EXIT_UNSET: + default: + break; + } +} + +static hybbx_result_t tnc_enter_kiss_mode(hybbx_tnc_t *tnc) +{ + return tnc_enter_kiss_by_entry(tnc, tnc->config.params.kiss_entry); +} + +static void tnc_deliver_ui(hybbx_tnc_t *tnc, const uint8_t *frame, size_t len) +{ + uint8_t payload[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t payload_len; + + payload_len = hybbx_ax25_parse_ui(frame, len, &path, payload, + sizeof(payload)); + if (payload_len == 0) { + return; + } + + if (tnc->ui_cb != NULL) { + tnc->ui_cb(payload, payload_len, &path, tnc->rx_userdata); + } + + tnc_note_rx(tnc); +} + +static void on_kiss_frame(uint8_t port, const uint8_t *frame, size_t len, + void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + uint8_t payload[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t payload_len; + + (void)port; + + if (tnc->frame_cb != NULL) { + tnc->frame_cb(frame, len, tnc->rx_userdata); + } + + if (tnc->ui_cb == NULL) { + return; + } + + payload_len = hybbx_ax25_parse_ui(frame, len, &path, payload, + sizeof(payload)); + if (payload_len > 0) { + tnc->ui_cb(payload, payload_len, &path, tnc->rx_userdata); + } +} + +static void on_sixpack_frame(const uint8_t *frame, size_t len, void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + + if (tnc->frame_cb != NULL) { + tnc->frame_cb(frame, len, tnc->rx_userdata); + } + + if (tnc->ui_cb != NULL) { + tnc_deliver_ui(tnc, frame, len); + } +} + +static void on_host_text(const uint8_t *data, size_t len, void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + + if (tnc->ui_cb != NULL) { + tnc->ui_cb(data, len, NULL, tnc->rx_userdata); + } + + tnc_note_rx(tnc); +} + +static void on_host_event(hybbx_tnc_host_state_t state, void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + + if (state == HYBBX_TNC_HOST_CMD) { + tnc->host_ready = 1; + } + + hybbx_log_debug("[tnc] host state -> %d", (int)state); +} + +static hybbx_result_t tnc_build_tx_path(hybbx_tnc_t *tnc) +{ + unsigned i; + hybbx_result_t rc; + + memset(&tnc->tx_path, 0, sizeof(tnc->tx_path)); + + if (tnc->config.dest_call != NULL && tnc->config.dest_call[0] != '\0') { + rc = hybbx_ax25_address_parse(tnc->config.dest_call, + &tnc->tx_path.dest); + if (rc != HYBBX_OK) { + return rc; + } + } else { + (void)hybbx_ax25_address_parse("QST", &tnc->tx_path.dest); + } + + if (tnc->config.mycall != NULL && tnc->config.mycall[0] != '\0') { + rc = hybbx_ax25_address_parse(tnc->config.mycall, + &tnc->tx_path.source); + if (rc != HYBBX_OK) { + return rc; + } + } else { + (void)hybbx_ax25_address_parse("HYBBX", &tnc->tx_path.source); + } + + for (i = 0; i < tnc->config.via_count; i++) { + rc = hybbx_ax25_address_parse(tnc->config.via[i], + &tnc->tx_path.digi[i]); + if (rc != HYBBX_OK) { + return rc; + } + } + tnc->tx_path.digi_count = tnc->config.via_count; + + return HYBBX_OK; +} + +static const char *serial_parity_letter(hybbx_tnc_serial_parity_t parity) +{ + switch (parity) { + case HYBBX_TNC_SERIAL_PARITY_EVEN: + return "E"; + case HYBBX_TNC_SERIAL_PARITY_ODD: + return "O"; + case HYBBX_TNC_SERIAL_PARITY_NONE: + default: + return "N"; + } +} + +static void tnc_serial_params_from_config(const hybbx_packet_radio_config_t *config, + hybbx_serial_params_t *out) +{ + hybbx_serial_params_default(out, config->baud); + out->data_bits = config->params.data_bits; + out->stop_bits = config->params.stop_bits; + out->assert_modem_lines = config->params.assert_modem_lines; + + switch (config->params.serial_parity) { + case HYBBX_TNC_SERIAL_PARITY_EVEN: + out->parity = HYBBX_SERIAL_PARITY_EVEN; + break; + case HYBBX_TNC_SERIAL_PARITY_ODD: + out->parity = HYBBX_SERIAL_PARITY_ODD; + break; + case HYBBX_TNC_SERIAL_PARITY_NONE: + default: + out->parity = HYBBX_SERIAL_PARITY_NONE; + break; + } +} + +static const char *tnc_profile_name(hybbx_packet_radio_tnc_t tnc) +{ + return hybbx_packet_radio_tnc_name(tnc); +} + +static const char *modem_name(hybbx_tnc2c_modem_t modem) +{ + switch (modem) { + case HYBBX_TNC2C_MODEM_9600: + return "9600"; + case HYBBX_TNC2C_MODEM_19200: + return "19200"; + case HYBBX_TNC2C_MODEM_TCM3105: + default: + return "tcm3105"; + } +} + + +static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc); + +static hybbx_result_t tnc_profile_init(hybbx_tnc_t *tnc) +{ + hybbx_result_t rc; + char cmd[96]; + + rc = tnc_build_tx_path(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + if (tnc->config.params.kiss_entry == HYBBX_KISS_ENTRY_NONE) { + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_NONE; + hybbx_log_info("[tnc] KISS attach (MAX25 prep assumed)"); + return HYBBX_OK; + } + + if (tnc_profile_thefirmware(tnc->config.tnc)) { + (void)tnc_recover_terminal_mode(tnc); + hybbx_log_info("[tnc] TheFirmware terminal recovery before KISS entry"); + } + + rc = tnc_enter_kiss_mode(tnc); + if (rc != HYBBX_OK) { + return rc; + } + return tnc_apply_kiss_params(tnc); + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) { + tnc->kiss_active = 0; + return HYBBX_OK; + } + + /* Host mode setup (firmware TNC profiles only; not BayCom/based SER12 / PC-COM modem). */ + drain_serial(tnc, 4); + (void)send_cmd(tnc, ""); + + if (tnc->config.mycall != NULL && tnc->config.mycall[0] != '\0') { + snprintf(cmd, sizeof(cmd), "MYCALL %s", tnc->config.mycall); + rc = send_cmd(tnc, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + snprintf(cmd, sizeof(cmd), "TXDELAY %u", tnc->config.params.txdelay); + (void)send_cmd(tnc, cmd); + snprintf(cmd, sizeof(cmd), "Persist %u", tnc->config.params.persist); + (void)send_cmd(tnc, cmd); + snprintf(cmd, sizeof(cmd), "SlotTime %u", tnc->config.params.slot); + (void)send_cmd(tnc, cmd); + snprintf(cmd, sizeof(cmd), "TXTAIL %u", tnc->config.params.txtail); + (void)send_cmd(tnc, cmd); + + if (tnc->config.params.fullduplex) { + (void)send_cmd(tnc, "FULLDUP ON"); + } else { + (void)send_cmd(tnc, "FULLDUP OFF"); + } + + if (tnc->config.tnc == HYBBX_PACKET_RADIO_TNC_PCCOM) { + (void)send_cmd(tnc, "PACLEN 256"); + (void)send_cmd(tnc, "MAXFRAME 4"); + } + + if (tnc->config.tnc == HYBBX_PACKET_RADIO_TNC_BAYCOM) { + (void)send_cmd(tnc, "PACLEN 256"); + } + + if (tnc->config.params.host_connect_on_start && + tnc->config.dest_call != NULL && tnc->config.dest_call[0] != '\0') { + snprintf(cmd, sizeof(cmd), "C %s", tnc->config.dest_call); + rc = send_cmd(tnc, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + drain_serial(tnc, 6); + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_open(hybbx_tnc_t **out, + const hybbx_packet_radio_config_t *config, + hybbx_tnc_frame_cb frame_cb, + hybbx_tnc_ui_cb ui_cb, + void *rx_userdata) +{ + hybbx_tnc_t *tnc; + hybbx_result_t rc; + + if (out == NULL || config == NULL || config->device == NULL) { + return HYBBX_ERR_INVALID; + } + + if (frame_cb == NULL && ui_cb == NULL) { + return HYBBX_ERR_INVALID; + } + + tnc = calloc(1, sizeof(*tnc)); + if (tnc == NULL) { + return HYBBX_ERR_NOMEM; + } + + config_copy(&tnc->config, config); + tnc->frame_cb = frame_cb; + tnc->ui_cb = ui_cb; + tnc->rx_userdata = rx_userdata; + + hybbx_kiss_decoder_init(&tnc->kiss_dec); + hybbx_sixpack_decoder_init(&tnc->sixpack_dec); + hybbx_tnc_host_init(&tnc->host, on_host_text, on_host_event, tnc); + + hybbx_serial_params_t serial_params; + + tnc_serial_params_from_config(config, &serial_params); + + rc = hybbx_serial_open(&tnc->serial, config->device, &serial_params); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] cannot open %s at %u baud (%u%c%u) — " + "device not available or permission denied", + config->device, config->baud, serial_params.data_bits, + *serial_parity_letter(config->params.serial_parity), + serial_params.stop_bits); + free(tnc); + return rc; + } + + hybbx_log_info("[tnc] profile=%s protocol=%u device=%s host=%u %u%c%u rts_dtr=%s " + "kiss_entry=%s radio=%u modem=%s modulation=%s band=%s duplex=%s " + "txdelay=%u persist=%u", + tnc_profile_name(config->tnc), (unsigned)config->protocol, + config->device, config->baud, serial_params.data_bits, + *serial_parity_letter(config->params.serial_parity), + serial_params.stop_bits, + serial_params.assert_modem_lines ? "on" : "off", + hybbx_kiss_entry_name(config->params.kiss_entry), + config->params.radio_baud, + modem_name(config->params.modem), + hybbx_packet_radio_modulation_name(config->params.modulation), + hybbx_packet_radio_band_name(config->params.band), + hybbx_packet_radio_duplex_name(config->params.duplex), + config->params.txdelay, + config->params.persist); + + if (config->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C && + serial_params.data_bits == 8 && + config->params.serial_parity == HYBBX_TNC_SERIAL_PARITY_NONE) { + hybbx_log_warn("[tnc] tnc2c %s: 8N1 host line — use serial_line=7E1 if KISS/UNPROTO TX fails", + config->device); + } + + rc = tnc_profile_init(tnc); + if (rc != HYBBX_OK) { + hybbx_tnc_close(tnc); + return rc; + } + + if (config->protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + hybbx_log_info("[tnc] KISS active (port %u)", config->params.kiss_port); + if (tnc_profile_thefirmware(config->tnc)) { + hybbx_log_info("[tnc] UI transmit=TheFirmware UNPROTO (KISS for RX)"); + } + } else if (config->protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) { + hybbx_log_info("[tnc] 6PACK active"); + } else { + hybbx_log_info("[tnc] TNC2 host mode active"); + } + + *out = tnc; + return HYBBX_OK; +} + +void hybbx_tnc_close(hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return; + } + + if (tnc->serial != NULL) { + tnc_exit_kiss_mode(tnc); + hybbx_serial_close(tnc->serial); + } + + free(tnc); +} + +int hybbx_tnc_serial_fd(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL || tnc->serial == NULL) { + return -1; + } + + return hybbx_serial_fd(tnc->serial); +} + +static hybbx_result_t tnc_ensure_kiss_tx(hybbx_tnc_t *tnc) +{ + hybbx_result_t rc; + + if (tnc == NULL || + tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) { + return HYBBX_OK; + } + + if (!tnc->kiss_active) { + rc = tnc_enter_kiss_mode(tnc); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] KISS TX blocked — host mode (re-entry failed)"); + return rc; + } + hybbx_log_info("[tnc] KISS re-entered before RF TX"); + return tnc_apply_kiss_params(tnc); + } + + return HYBBX_OK; +} + +/** KISS DATA: host sends AX.25 body only; TheFirmware adds FCS on air. */ +static size_t tnc_kiss_ax25_len(const uint8_t *frame, size_t len) +{ + uint16_t crc_rx; + uint16_t crc_calc; + + if (len < 16) { + return len; + } + + crc_rx = (uint16_t)frame[len - 2] | ((uint16_t)frame[len - 1] << 8); + crc_calc = hybbx_ax25_crc(frame, len - 2); + if (crc_rx == crc_calc) { + return len - 2; + } + + return len; +} + +static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_TNC2: + case HYBBX_PACKET_RADIO_TNC_TNC2C: + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return 1; + default: + return 0; + } +} + +static void tnc_format_ax25_call(const hybbx_ax25_address_t *addr, + char *out, size_t out_len) +{ + if (addr == NULL || out == NULL || out_len == 0) { + return; + } + + snprintf(out, out_len, "%s", addr->call); + if (addr->ssid > 0) { + size_t n = strlen(out); + + if (n + 4 < out_len) { + snprintf(out + n, out_len - n, "-%u", addr->ssid); + } + } +} + +/** + * TheFirmware (TNC-2 class) often ignores KISS DATA on hybrid host/KISS builds + * while UNPROTO from cmd: mode keys PTT reliably. Exit KISS, send, re-enter. + */ +static hybbx_result_t tnc_thefirmware_unproto_send(hybbx_tnc_t *tnc, + const hybbx_ax25_path_t *path, + const uint8_t *payload, + size_t payload_len) +{ + char cmd[320]; + char dest[HYBBX_AX25_CALL_MAX + 8]; + hybbx_result_t rc; + + if (tnc == NULL || path == NULL || payload == NULL || payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (payload_len + 64 >= sizeof(cmd)) { + return HYBBX_ERR_INVALID; + } + + tnc_format_ax25_call(&path->dest, dest, sizeof(dest)); + + rc = tnc_kiss_wakeup_host(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + (void)send_cmd(tnc, "MON OFF"); + + snprintf(cmd, sizeof(cmd), "UNPROTO %s 0 %.*s", + dest, (int)payload_len, (const char *)payload); + rc = send_cmd(tnc, cmd); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] UNPROTO %s host send failed", dest); + return rc; + } + + drain_serial(tnc, 24); + tnc_serial_pause_ms(500); + + hybbx_log_info("[tnc] TheFirmware UNPROTO %s (%zu bytes)", dest, payload_len); + + rc = tnc_enter_kiss_mode(tnc); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] KISS re-entry after UNPROTO failed"); + return rc; + } + + return tnc_apply_kiss_params(tnc); +} + +hybbx_result_t hybbx_tnc_send_frame(hybbx_tnc_t *tnc, + const uint8_t *frame, size_t len) +{ + uint8_t buf[HYBBX_KISS_MAX_FRAME + 8]; + size_t encoded; + hybbx_result_t rc; + + if (tnc == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_rf_tx_pace(); + + rc = tnc_wait_half_duplex_clear(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) { + encoded = hybbx_sixpack_encode(frame, len, buf, sizeof(buf)); + if (encoded == 0) { + return HYBBX_ERR_IO; + } + return send_raw(tnc, buf, encoded); + } + + if (!tnc->kiss_active && + tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS && + tnc_profile_thefirmware(tnc->config.tnc)) { + hybbx_ax25_path_t path; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + size_t ui_len = hybbx_ax25_parse_ui(frame, len, &path, ui, sizeof(ui)); + + if (ui_len > 0) { + return tnc_thefirmware_unproto_send(tnc, &path, ui, ui_len); + } + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + rc = tnc_ensure_kiss_tx(tnc); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + const uint8_t *tx_frame = frame; + size_t tx_len = len; + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + tx_len = tnc_kiss_ax25_len(frame, len); + tx_frame = frame; + (void)send_kiss_param(tnc, HYBBX_KISS_CMD_PERSIST, + (uint8_t)tnc->config.params.persist); + if (tnc_profile_thefirmware(tnc->config.tnc)) { + hybbx_log_warn("[tnc] UI parse failed (%zu bytes) — KISS DATA fallback", + len); + } + } + + encoded = hybbx_kiss_encode((uint8_t)tnc->config.params.kiss_port, + HYBBX_KISS_CMD_DATA, tx_frame, tx_len, + buf, sizeof(buf)); + if (encoded == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[tnc] KISS DATA ax25=%zu kiss=%zu%s", + tx_len, encoded, + tx_len + 2 == len ? " (fcs stripped)" : ""); + + return send_raw(tnc, buf, encoded); + } +} + +hybbx_result_t hybbx_tnc_send_ui(hybbx_tnc_t *tnc, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_AX25_FRAME_MAX]; + size_t frame_len; + + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_ax25_build_ui(&tnc->tx_path, payload, len, + frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_tnc_send_frame(tnc, frame, frame_len); +} + +hybbx_result_t hybbx_tnc_send_converse(hybbx_tnc_t *tnc, + const char *data, size_t len) +{ + hybbx_result_t rc; + + if (tnc == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = tnc_wait_half_duplex_clear(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + return send_raw(tnc, (const uint8_t *)data, len); +} + +hybbx_packet_radio_duplex_t hybbx_tnc_duplex(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + return tnc->config.params.duplex; +} + +hybbx_packet_radio_band_t hybbx_tnc_band(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_PACKET_RADIO_BAND_UNSET; + } + + return tnc->config.params.band; +} + +int hybbx_tnc_duplex_is_full(const hybbx_tnc_t *tnc) +{ + return tnc != NULL && + tnc->config.params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL; +} + +int hybbx_tnc_host_connected(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return 0; + } + + return tnc->host.state == HYBBX_TNC_HOST_CONNECTED; +} + +hybbx_result_t hybbx_tnc_poll(hybbx_tnc_t *tnc) +{ + uint8_t buf[256]; + size_t read_len; + hybbx_result_t rc; + + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_serial_read(tnc->serial, buf, sizeof(buf), &read_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (read_len == 0) { + return HYBBX_OK; + } + + switch (tnc->config.protocol) { + case HYBBX_PACKET_RADIO_PROTO_KISS: + hybbx_kiss_decoder_feed(&tnc->kiss_dec, buf, read_len, + on_kiss_frame, tnc); + break; + case HYBBX_PACKET_RADIO_PROTO_SIXPACK: + hybbx_sixpack_decoder_feed(&tnc->sixpack_dec, buf, read_len, + on_sixpack_frame, tnc); + break; + case HYBBX_PACKET_RADIO_PROTO_HOSTMODE: + hybbx_tnc_host_feed(&tnc->host, buf, read_len); + break; + default: + break; + } + + return HYBBX_OK; +} diff --git a/plugins/packet_radio/tnc_host.c b/plugins/packet_radio/tnc_host.c new file mode 100644 index 0000000..8700e61 --- /dev/null +++ b/plugins/packet_radio/tnc_host.c @@ -0,0 +1,143 @@ +#include "tnc_host.h" + +#include +#include + +void hybbx_tnc_host_init(hybbx_tnc_host_t *host, + hybbx_tnc_host_text_cb text_cb, + hybbx_tnc_host_event_cb event_cb, + void *userdata) +{ + if (host == NULL) { + return; + } + + memset(host, 0, sizeof(*host)); + host->state = HYBBX_TNC_HOST_BOOT; + host->text_cb = text_cb; + host->event_cb = event_cb; + host->userdata = userdata; +} + +void hybbx_tnc_host_reset(hybbx_tnc_host_t *host) +{ + if (host == NULL) { + return; + } + + host->line_len = 0; + host->line[0] = '\0'; + host->state = HYBBX_TNC_HOST_BOOT; +} + +static int host_line_ieq(const char *line, const char *prefix) +{ + size_t i; + size_t plen = strlen(prefix); + + for (i = 0; i < plen; i++) { + char a = (char)tolower((unsigned char)line[i]); + char b = (char)tolower((unsigned char)prefix[i]); + + if (a != b) { + return 0; + } + } + + return 1; +} + +static void host_emit_event(hybbx_tnc_host_t *host, hybbx_tnc_host_state_t state) +{ + if (host->state == state) { + return; + } + + host->state = state; + if (host->event_cb != NULL) { + host->event_cb(state, host->userdata); + } +} + +static void host_process_line(hybbx_tnc_host_t *host) +{ + const char *line = host->line; + + if (host_line_ieq(line, "cmd:") || host_line_ieq(line, "cmd: ")) { + host_emit_event(host, HYBBX_TNC_HOST_CMD); + return; + } + + if (strstr(line, "***") != NULL && + (host_line_ieq(line, "*** connected") || + host_line_ieq(line, "*** connected ") || + strstr(line, "CONNECTED") != NULL)) { + host_emit_event(host, HYBBX_TNC_HOST_CONNECTED); + return; + } + + if (strstr(line, "***") != NULL && + (host_line_ieq(line, "*** disconnected") || + strstr(line, "DISCONNECTED") != NULL)) { + host_emit_event(host, HYBBX_TNC_HOST_CMD); + return; + } + + if (host->state == HYBBX_TNC_HOST_CONNECTED && host->text_cb != NULL && + line[0] != '\0') { + host->text_cb((const uint8_t *)line, strlen(line), host->userdata); + host->text_cb((const uint8_t *)"\r\n", 2, host->userdata); + } +} + +static void host_push_byte(hybbx_tnc_host_t *host, uint8_t byte) +{ + if (byte == '\r' || byte == '\n') { + if (host->line_len > 0) { + host->line[host->line_len] = '\0'; + host_process_line(host); + host->line_len = 0; + } + return; + } + + if (host->line_len + 1 >= sizeof(host->line)) { + host->line_len = 0; + return; + } + + host->line[host->line_len++] = (char)byte; +} + +void hybbx_tnc_host_feed(hybbx_tnc_host_t *host, + const uint8_t *data, size_t len) +{ + size_t i; + + if (host == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + host_push_byte(host, data[i]); + } +} + +size_t hybbx_tnc_host_format_cmd(const char *cmd, char *out, size_t out_cap) +{ + size_t len; + + if (cmd == NULL || out == NULL || out_cap < 3) { + return 0; + } + + len = strlen(cmd); + if (len + 2 >= out_cap) { + return 0; + } + + memcpy(out, cmd, len); + out[len++] = '\r'; + out[len] = '\0'; + return len; +} diff --git a/plugins/packet_radio/tnc_host.h b/plugins/packet_radio/tnc_host.h new file mode 100644 index 0000000..af55505 --- /dev/null +++ b/plugins/packet_radio/tnc_host.h @@ -0,0 +1,51 @@ +#ifndef HYBBX_TNC_HOST_H +#define HYBBX_TNC_HOST_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum hybbx_tnc_host_state { + HYBBX_TNC_HOST_BOOT = 0, + HYBBX_TNC_HOST_CMD = 1, + HYBBX_TNC_HOST_CONNECTED = 2 +} hybbx_tnc_host_state_t; + +typedef void (*hybbx_tnc_host_text_cb)(const uint8_t *data, size_t len, + void *userdata); + +typedef void (*hybbx_tnc_host_event_cb)(hybbx_tnc_host_state_t state, + void *userdata); + +typedef struct hybbx_tnc_host { + hybbx_tnc_host_state_t state; + char line[256]; + size_t line_len; + hybbx_tnc_host_text_cb text_cb; + hybbx_tnc_host_event_cb event_cb; + void *userdata; +} hybbx_tnc_host_t; + +void hybbx_tnc_host_init(hybbx_tnc_host_t *host, + hybbx_tnc_host_text_cb text_cb, + hybbx_tnc_host_event_cb event_cb, + void *userdata); + +void hybbx_tnc_host_reset(hybbx_tnc_host_t *host); + +void hybbx_tnc_host_feed(hybbx_tnc_host_t *host, + const uint8_t *data, size_t len); + +/** Build a TNC2 command line (adds CR). Returns length or 0. */ +size_t hybbx_tnc_host_format_cmd(const char *cmd, char *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TNC_HOST_H */ diff --git a/plugins/packet_radio/tnc_profiles.c b/plugins/packet_radio/tnc_profiles.c new file mode 100644 index 0000000..05a2397 --- /dev/null +++ b/plugins/packet_radio/tnc_profiles.c @@ -0,0 +1,399 @@ +#include "hybbx/tnc.h" +#include "hybbx/log.h" + +#include + +const char *hybbx_packet_radio_duplex_name(hybbx_packet_radio_duplex_t duplex) +{ + switch (duplex) { + case HYBBX_PACKET_RADIO_DUPLEX_FULL: + return "full"; + case HYBBX_PACKET_RADIO_DUPLEX_HALF: + return "half"; + case HYBBX_PACKET_RADIO_DUPLEX_UNSET: + default: + return "unset"; + } +} + +const char *hybbx_packet_radio_band_name(hybbx_packet_radio_band_t band) +{ + switch (band) { + case HYBBX_PACKET_RADIO_BAND_CB: + return "cb"; + case HYBBX_PACKET_RADIO_BAND_AMATEUR: + return "amateur"; + case HYBBX_PACKET_RADIO_BAND_UNSET: + default: + return "unset"; + } +} + +const char *hybbx_packet_radio_modulation_name( + hybbx_packet_radio_modulation_t modulation) +{ + switch (modulation) { + case HYBBX_PACKET_RADIO_MOD_G3RUH_FSK: + return "g3ruh_fsk"; + case HYBBX_PACKET_RADIO_MOD_AFSK: + return "afsk"; + case HYBBX_PACKET_RADIO_MOD_UNSET: + default: + return "unset"; + } +} + +int hybbx_packet_radio_modulation_is_g3ruh( + hybbx_packet_radio_modulation_t modulation) +{ + return modulation == HYBBX_PACKET_RADIO_MOD_G3RUH_FSK; +} + +hybbx_result_t hybbx_tnc_finalize_modulation(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.modulation == HYBBX_PACKET_RADIO_MOD_UNSET) { + cfg->params.modulation = HYBBX_PACKET_RADIO_MOD_AFSK; + } + + if (cfg->params.modulation == HYBBX_PACKET_RADIO_MOD_G3RUH_FSK) { + cfg->params.modem = HYBBX_TNC2C_MODEM_9600; + cfg->params.radio_baud = HYBBX_G3RUH_RADIO_BAUD; + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C && + cfg->params.clock_mhz <= HYBBX_TNC2C_CLOCK_4_9_MHZ) { + cfg->params.clock_mhz = HYBBX_TNC2C_CLOCK_9_8_MHZ; + } + cfg->params.txdelay = 15; + cfg->params.persist = 128; + cfg->params.slot = 3; + return HYBBX_OK; + } + + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_AFSK_RADIO_BAUD; + } + if (cfg->params.modem == 0) { + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + } + + return HYBBX_OK; +} + +static hybbx_packet_radio_band_t default_band_for_tnc(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_PCCOM: + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return HYBBX_PACKET_RADIO_BAND_CB; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + case HYBBX_PACKET_RADIO_TNC_TNC2: + case HYBBX_PACKET_RADIO_TNC_PK232: + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + case HYBBX_PACKET_RADIO_TNC_GENERIC: + default: + return HYBBX_PACKET_RADIO_BAND_AMATEUR; + } +} + +const char *hybbx_packet_radio_tnc_name(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return "baycom"; + case HYBBX_PACKET_RADIO_TNC_PCCOM: + return "pccom"; + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return "generic"; + case HYBBX_PACKET_RADIO_TNC_TNC2: + return "tnc2"; + case HYBBX_PACKET_RADIO_TNC_PK232: + return "pk232"; + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + return "mfj1278"; + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + return "kantronics"; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + return "tnc2c"; + } +} + +const char *hybbx_kiss_entry_name(hybbx_kiss_entry_t entry) +{ + switch (entry) { + case HYBBX_KISS_ENTRY_NONE: + return "none"; + case HYBBX_KISS_ENTRY_KISS_ON: + return "kiss_on"; + case HYBBX_KISS_ENTRY_ESC_AT_K: + return "esc_at_k"; + case HYBBX_KISS_ENTRY_AUTO: + return "auto"; + case HYBBX_KISS_ENTRY_UNSET: + default: + return "unset"; + } +} + +static hybbx_kiss_entry_t default_kiss_entry_for_tnc(hybbx_packet_radio_tnc_t tnc) +{ + (void)tnc; + return HYBBX_KISS_ENTRY_NONE; +} + +hybbx_result_t hybbx_tnc_finalize_kiss_entry(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.kiss_entry == HYBBX_KISS_ENTRY_UNSET) { + cfg->params.kiss_entry = default_kiss_entry_for_tnc(cfg->tnc); + } + + if (cfg->params.kiss_exit == HYBBX_KISS_EXIT_UNSET) { + if (cfg->params.kiss_entry == HYBBX_KISS_ENTRY_NONE) { + cfg->params.kiss_exit = HYBBX_KISS_EXIT_NONE; + } else { + cfg->params.kiss_exit = HYBBX_KISS_EXIT_AUTO; + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_radio_duplex(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = default_band_for_tnc(cfg->tnc); + } + + if (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB && + cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) { + hybbx_log_warn("[tnc] radio_band=cb allows half-duplex only; " + "ignoring radio_duplex=full"); + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + cfg->params.fullduplex = + (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) ? 1 : 0; + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_csma(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.persist == 0) { + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB) { + cfg->params.persist = 255; + } else { + cfg->params.persist = 63; + } + } else if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB && + cfg->params.persist < 255) { + hybbx_log_warn("[tnc] CB band persist=%u overridden to 255 for beacon TX", + cfg->params.persist); + cfg->params.persist = 255; + } + + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 50; + } + + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_serial_line(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.data_bits == 0) { + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C || + cfg->tnc == HYBBX_PACKET_RADIO_TNC_MFJ1278) { + cfg->params.data_bits = HYBBX_TNC2C_DATA_BITS; + } else { + cfg->params.data_bits = 8; + } + } + + if (cfg->params.serial_parity == HYBBX_TNC_SERIAL_PARITY_UNSET) { + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C || + cfg->tnc == HYBBX_PACKET_RADIO_TNC_MFJ1278) { + cfg->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_EVEN; + } else { + cfg->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_NONE; + } + } + + if (cfg->params.stop_bits == 0) { + cfg->params.stop_bits = 1; + } + + if (cfg->params.assert_modem_lines < 0) { + cfg->params.assert_modem_lines = + (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C) ? 1 : 0; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_profile_apply_defaults(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (cfg->tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = HYBBX_PACKET_RADIO_BAND_CB; + } + cfg->params.radio_baud = 1200; + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 30; + } + if (cfg->params.persist == 0) { + cfg->params.persist = 63; + } + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + break; + + case HYBBX_PACKET_RADIO_TNC_PCCOM: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = HYBBX_PACKET_RADIO_BAND_CB; + } + cfg->params.radio_baud = 1200; + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_HOSTMODE; + } + if (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 25; + } + if (cfg->params.persist == 0) { + cfg->params.persist = 128; + } + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + cfg->params.host_connect_on_start = 1; + break; + + case HYBBX_PACKET_RADIO_TNC_GENERIC: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_TNC2: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_PK232: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + if (cfg->baud == 0) { + cfg->baud = 4800; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->params.clock_mhz == 0.0f) { + cfg->params.clock_mhz = HYBBX_TNC2C_CLOCK_4_9_MHZ; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + } + + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + + (void)hybbx_tnc_finalize_modulation(cfg); + (void)hybbx_tnc_finalize_radio_duplex(cfg); + (void)hybbx_tnc_finalize_csma(cfg); + (void)hybbx_tnc_finalize_serial_line(cfg); + return hybbx_tnc_finalize_kiss_entry(cfg); +} diff --git a/plugins/ssh/CMakeLists.txt b/plugins/ssh/CMakeLists.txt new file mode 100644 index 0000000..9c656b8 --- /dev/null +++ b/plugins/ssh/CMakeLists.txt @@ -0,0 +1,35 @@ +add_library(hybbx_plugin_ssh STATIC + ssh.c + ssh_config.c + ssh_keys.c +) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_ssh + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_ssh + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_ssh + PRIVATE + hybbx_core + Threads::Threads + PkgConfig::LIBSSH +) + +hybbx_link_plugin_instances(hybbx_plugin_ssh HYBBX_HAVE_PLUGIN_SSH) +hybbx_apply_hardening(hybbx_plugin_ssh) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_ssh + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/ssh/ssh.c b/plugins/ssh/ssh.c new file mode 100644 index 0000000..442653d --- /dev/null +++ b/plugins/ssh/ssh.c @@ -0,0 +1,728 @@ +/* + * ssh — libssh transport plugin (port 3232). INI: [transport.ssh]. + * + * Links dynamically against libssh (LGPL-2.1+). See share/THIRD_PARTY_NOTICES.txt. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/ssh.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SSH_CLIENT_POLL_MS 30000 +#define SSH_READ_BUF 512 + +typedef struct ssh_client { + int fd; + ssh_session session; + ssh_channel channel; + ssh_event event; + hybbx_session_t *hbx_session; + int ssh_authenticated; + int auth_attempts; + hybbx_result_t last_rc; + volatile int shell_ready; + struct ssh_server_callbacks_struct server_cb; + struct ssh_channel_callbacks_struct channel_cb; +} ssh_client_t; + +typedef struct ssh_client_ctx { + ssh_client_t *client; +} ssh_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_ssh; + +static hybbx_service_t *g_service; +static hybbx_ssh_config_t g_config; +static char g_hostkey_path[HYBBX_PATH_MAX]; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_ssh_running = 0; + +static hybbx_result_t ssh_plugin_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +/* + * SSH wire authentication only — accepts any username/password so the client + * can open a shell channel. HyBBX login (guest auto-login or /login prompt) + * follows [auth] in hybbx.ini, same as telnet. + */ +static int ssh_auth_password(ssh_session session, const char *user, + const char *password, void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + (void)user; + (void)password; + + if (client == NULL) { + return SSH_AUTH_DENIED; + } + + client->auth_attempts++; + client->ssh_authenticated = 1; + return SSH_AUTH_SUCCESS; +} + +static hybbx_result_t ssh_plugin_stop(void); + +static int ssh_channel_pty_request(ssh_session session, ssh_channel channel, + const char *term, int cols, int rows, + int py, int px, void *userdata) +{ + (void)session; + (void)channel; + (void)term; + (void)cols; + (void)rows; + (void)py; + (void)px; + (void)userdata; + return SSH_OK; +} + +static int ssh_channel_shell_request(ssh_session session, ssh_channel channel, + void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + (void)channel; + + if (client == NULL) { + return SSH_ERROR; + } + + client->shell_ready = 1; + return SSH_OK; +} + +static ssh_channel ssh_channel_open(ssh_session session, void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + + if (client == NULL) { + return NULL; + } + + client->channel = ssh_channel_new(session); + if (client->channel == NULL) { + return NULL; + } + + memset(&client->channel_cb, 0, sizeof(client->channel_cb)); + client->channel_cb.userdata = client; + client->channel_cb.channel_pty_request_function = ssh_channel_pty_request; + client->channel_cb.channel_shell_request_function = ssh_channel_shell_request; + ssh_callbacks_init(&client->channel_cb); + ssh_set_channel_callbacks(client->channel, &client->channel_cb); + + return client->channel; +} + +static hybbx_result_t ssh_plugin_write(hybbx_session_t *session, + const char *data, size_t len) +{ + ssh_client_t *client; + size_t i; + int rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (ssh_client_t *)session->transport_data; + if (client == NULL || client->channel == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char out[2]; + size_t out_len = 1; + + out[0] = data[i]; + if (data[i] == '\n' && (i == 0 || data[i - 1] != '\r')) { + out[0] = '\r'; + out[1] = '\n'; + out_len = 2; + } + + rc = ssh_channel_write(client->channel, out, out_len); + if (rc < 0) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} + +static void ssh_send_busy(ssh_channel channel) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (channel == NULL) { + return; + } + + (void)ssh_channel_write(channel, msg, sizeof(msg) - 1u); +} + +static int ssh_wait_auth_and_shell(ssh_client_t *client) +{ + int ticks = 0; + + while (g_ssh_running) { + if (client->ssh_authenticated && client->channel != NULL && + client->shell_ready) { + return 0; + } + + if (client->auth_attempts >= 5 || ticks >= 300) { + return -1; + } + + if (ssh_event_dopoll(client->event, 100) == SSH_ERROR) { + return -1; + } + ticks++; + } + + return -1; +} + +static void ssh_on_user_data(const uint8_t *data, size_t len, void *ctx) +{ + ssh_client_ctx_t *cctx = (ssh_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (cctx == NULL || cctx->client == NULL || + cctx->client->hbx_session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(cctx->client->hbx_session, data, len); + if (rc != HYBBX_OK) { + cctx->client->last_rc = rc; + } +} + +static int ssh_plugin_service_request(ssh_session session, const char *service, + void *userdata) +{ + (void)session; + (void)userdata; + + if (service == NULL) { + return SSH_ERROR; + } + + if (strcmp(service, "ssh-userauth") == 0 || + strcmp(service, "ssh-connection") == 0) { + return SSH_OK; + } + + return SSH_ERROR; +} + +static void *ssh_client_thread(void *arg) +{ + ssh_client_t *client = (ssh_client_t *)arg; + ssh_bind sshbind = NULL; + ssh_client_ctx_t cctx; + uint8_t buf[SSH_READ_BUF]; + hybbx_result_t rc; + char remote[64]; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + remote[0] = '\0'; + (void)hybbx_socket_peer_name(client->fd, remote, sizeof(remote)); + + sshbind = ssh_bind_new(); + if (sshbind == NULL) { + goto cleanup; + } + + if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, + g_hostkey_path) != SSH_OK) { + hybbx_log_warn("[ssh] host key load failed: %s", g_hostkey_path); + goto cleanup; + } + + client->session = ssh_new(); + if (client->session == NULL) { + goto cleanup; + } + + if (ssh_bind_accept_fd(sshbind, client->session, client->fd) != SSH_OK) { + goto cleanup; + } + + memset(&client->server_cb, 0, sizeof(client->server_cb)); + client->server_cb.userdata = client; + client->server_cb.auth_password_function = ssh_auth_password; + client->server_cb.service_request_function = ssh_plugin_service_request; + client->server_cb.channel_open_request_session_function = ssh_channel_open; + ssh_callbacks_init(&client->server_cb); + ssh_set_server_callbacks(client->session, &client->server_cb); + ssh_set_auth_methods(client->session, SSH_AUTH_METHOD_PASSWORD); + + if (ssh_server_init_kex(client->session) != SSH_OK) { + goto cleanup; + } + + if (ssh_handle_key_exchange(client->session) != SSH_OK) { + goto cleanup; + } + + client->event = ssh_event_new(); + if (client->event == NULL) { + goto cleanup; + } + + if (ssh_event_add_session(client->event, client->session) != SSH_OK) { + goto cleanup; + } + + if (ssh_wait_auth_and_shell(client) != 0) { + goto cleanup; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_ssh, client, + &client->hbx_session); + if (rc != HYBBX_OK || client->hbx_session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + ssh_send_busy(client->channel); + } + goto cleanup; + } + + if (remote[0] != '\0') { + (void)hybbx_session_set_remote(client->hbx_session, remote); + } + + /* + * Telnet clients echo locally when the server sends WONT ECHO; SSH PTY + * clients do not. Mirror telnet UX by enabling HyBBX input echo unless + * [traffic] input_echo=yes already turned it on. + */ + { + const hybbx_traffic_config_t *traffic = hybbx_traffic_config_get(); + + if (traffic == NULL || !traffic->input_echo) { + (void)hybbx_session_set_input_echo(client->hbx_session, 1); + } + } + + cctx.client = client; + client->last_rc = HYBBX_OK; + + while (g_ssh_running && ssh_channel_is_open(client->channel)) { + int n; + int ev; + + ev = ssh_event_dopoll(client->event, SSH_CLIENT_POLL_MS); + if (ev == SSH_ERROR) { + break; + } + + if (ev == 0) { + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + + while ((n = ssh_channel_read(client->channel, buf, sizeof(buf), 0)) > 0) { + ssh_on_user_data((const uint8_t *)buf, (size_t)n, &cctx); + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + } + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + if (n < 0 && !ssh_channel_is_open(client->channel)) { + break; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + +cleanup: + if (client->hbx_session != NULL) { + hybbx_session_close(client->hbx_session); + client->hbx_session = NULL; + } + + if (client->channel != NULL) { + if (ssh_channel_is_open(client->channel)) { + ssh_channel_send_eof(client->channel); + ssh_channel_close(client->channel); + } + ssh_channel_free(client->channel); + client->channel = NULL; + } + + if (client->event != NULL) { + ssh_event_free(client->event); + client->event = NULL; + } + + if (client->session != NULL) { + ssh_disconnect(client->session); + ssh_free(client->session); + client->session = NULL; + } + + if (sshbind != NULL) { + ssh_bind_free(sshbind); + sshbind = NULL; + } + + if (client->fd >= 0) { + client->fd = -1; + } + + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + ssh_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + client->fd = client_fd; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, ssh_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *ssh_accept_thread(void *arg) +{ + (void)arg; + + while (g_ssh_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t ssh_plugin_init(hybbx_service_t *service) +{ + int rc; + + g_service = service; + + rc = ssh_init(); + if (rc < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void ssh_plugin_shutdown(void) +{ + ssh_plugin_stop(); + ssh_finalize(); +} + +static hybbx_result_t ssh_plugin_start(const char *config) +{ + char keys_resolved[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (g_ssh_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_ssh_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + if (hybbx_path_resolve(keys_resolved, sizeof(keys_resolved), + g_config.hostkey_dir) != HYBBX_OK) { + hybbx_strlcpy(keys_resolved, g_config.hostkey_dir, + sizeof(keys_resolved)); + } + + rc = hybbx_ssh_keys_ensure(keys_resolved, g_hostkey_path, + sizeof(g_hostkey_path)); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, + g_config.port); + if (g_listen_v4 < 0) { + hybbx_socket_log_bind_failure("ssh", g_config.bind_v4, + g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, + g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[ssh] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + g_ssh_running = 1; + + if (pthread_create(&g_accept_thread, NULL, ssh_accept_thread, NULL) != 0) { + g_ssh_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[256]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[ssh] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " (libssh)"); + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t ssh_plugin_stop(void) +{ + if (!g_ssh_running) { + return HYBBX_OK; + } + + g_ssh_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_log_info("[ssh] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_ssh = { + .name = "ssh", + .kind = HYBBX_TRANSPORT_SSH, + .version = 1, + .init = ssh_plugin_init, + .shutdown = ssh_plugin_shutdown, + .start = ssh_plugin_start, + .stop = ssh_plugin_stop, + .write = ssh_plugin_write, +}; diff --git a/plugins/ssh/ssh_config.c b/plugins/ssh/ssh_config.c new file mode 100644 index 0000000..77fb7db --- /dev/null +++ b/plugins/ssh/ssh_config.c @@ -0,0 +1,132 @@ +#include "hybbx/ssh.h" +#include "hybbx/util.h" + +#include +#include + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_SSH_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_SSH_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_ssh_config_defaults(hybbx_ssh_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_SSH_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_SSH_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + hybbx_strlcpy(config->hostkey_dir, HYBBX_SSH_DEFAULT_HOSTKEY_DIR, + sizeof(config->hostkey_dir)); + config->port = HYBBX_SSH_DEFAULT_PORT; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_ssh_config_parse(const char *config, + hybbx_ssh_config_t *out) +{ + char scratch[HYBBX_SSH_BIND_V6_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ssh_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "hostkey_dir", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->hostkey_dir, value, sizeof(out->hostkey_dir)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/ssh/ssh_keys.c b/plugins/ssh/ssh_keys.c new file mode 100644 index 0000000..656b9d4 --- /dev/null +++ b/plugins/ssh/ssh_keys.c @@ -0,0 +1,146 @@ +#include "hybbx/ssh.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include + +static hybbx_result_t mkdir_keys_dir(const char *keys_dir) +{ + char parent[HYBBX_PATH_MAX]; + struct stat st; + + if (keys_dir == NULL || keys_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (stat(keys_dir, &st) == 0) { + return S_ISDIR(st.st_mode) ? HYBBX_OK : HYBBX_ERR_IO; + } + + if (hybbx_path_dirname(keys_dir, parent, sizeof(parent)) == HYBBX_OK && + parent[0] != '\0' && strcmp(parent, keys_dir) != 0 && + stat(parent, &st) != 0) { + if (mkdir_keys_dir(parent) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + if (mkdir(keys_dir, 0700) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t generate_ed25519_keypair(const char *priv_path, + const char *pub_path) +{ + ssh_key key = NULL; + int rc; + + rc = ssh_pki_generate(SSH_KEYTYPE_ED25519, 0, &key); + if (rc != SSH_OK || key == NULL) { + hybbx_log_warn("[ssh] Ed25519 generate failed (rc=%d)", rc); + return HYBBX_ERR_IO; + } + + rc = ssh_pki_export_privkey_file(key, NULL, NULL, NULL, priv_path); + if (rc != SSH_OK) { + hybbx_log_warn("[ssh] export private key failed: %s", + ssh_get_error(key)); + ssh_key_free(key); + return HYBBX_ERR_IO; + } + + rc = ssh_pki_export_pubkey_file(key, pub_path); + ssh_key_free(key); + if (rc != SSH_OK) { + hybbx_log_warn("[ssh] export public key failed"); + return HYBBX_ERR_IO; + } + + (void)chmod(priv_path, 0600); + (void)chmod(pub_path, 0644); + return HYBBX_OK; +} + +static int hostkey_needs_rotation(const char *priv_path) +{ + struct stat st; + time_t now; + time_t age; + + if (stat(priv_path, &st) != 0) { + return 0; + } + + now = time(NULL); + if (now <= st.st_mtime) { + return 0; + } + + age = now - st.st_mtime; + return age > (time_t)HYBBX_SSH_HOSTKEY_VALID_DAYS * 86400L; +} + +hybbx_result_t hybbx_ssh_keys_ensure(const char *keys_dir, + char *hostkey_path, + size_t hostkey_path_len) +{ + char resolved_dir[HYBBX_PATH_MAX]; + char priv_path[HYBBX_PATH_MAX]; + char pub_path[HYBBX_PATH_MAX]; + struct stat st; + + if (keys_dir == NULL || hostkey_path == NULL || hostkey_path_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (ssh_init() < 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_resolve(resolved_dir, sizeof(resolved_dir), + keys_dir) != HYBBX_OK) { + hybbx_strlcpy(resolved_dir, keys_dir, sizeof(resolved_dir)); + } + + if (mkdir_keys_dir(resolved_dir) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(priv_path, sizeof(priv_path), resolved_dir, + HYBBX_SSH_HOSTKEY_ED25519) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(pub_path, sizeof(pub_path), resolved_dir, + HYBBX_SSH_HOSTKEY_ED25519 ".pub") != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (stat(priv_path, &st) == 0 && hostkey_needs_rotation(priv_path)) { + hybbx_log_warn("[ssh] host key older than %u days — rotating %s", + HYBBX_SSH_HOSTKEY_VALID_DAYS, priv_path); + (void)unlink(priv_path); + (void)unlink(pub_path); + } + + if (stat(priv_path, &st) != 0) { + if (generate_ed25519_keypair(priv_path, pub_path) != HYBBX_OK) { + hybbx_log_warn("[ssh] failed to generate host key in %s", + resolved_dir); + return HYBBX_ERR_IO; + } + hybbx_log_info("[ssh] generated host key %s (valid %u days)", priv_path, + HYBBX_SSH_HOSTKEY_VALID_DAYS); + } + + hybbx_strlcpy(hostkey_path, priv_path, hostkey_path_len); + return HYBBX_OK; +} diff --git a/plugins/telnet/CMakeLists.txt b/plugins/telnet/CMakeLists.txt new file mode 100644 index 0000000..144ba1d --- /dev/null +++ b/plugins/telnet/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(hybbx_plugin_telnet STATIC + telnet.c + telnet_config.c + telnet_proto.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_telnet + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_telnet + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_telnet PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_telnet HYBBX_HAVE_PLUGIN_TELNET) +hybbx_apply_hardening(hybbx_plugin_telnet) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_telnet + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/telnet/telnet.c b/plugins/telnet/telnet.c new file mode 100644 index 0000000..a857b7d --- /dev/null +++ b/plugins/telnet/telnet.c @@ -0,0 +1,516 @@ +/* + * telnet — TCP transport plugin (port 2323). INI: [transport.telnet]. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/telnet.h" +#include "telnet_proto.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct telnet_client { + int fd; + hybbx_telnet_parser_t parser; +} telnet_client_t; + +typedef struct telnet_client_ctx { + hybbx_session_t *session; + hybbx_result_t last_rc; +} telnet_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_telnet; + +static hybbx_service_t *g_service; +static hybbx_telnet_config_t g_config; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_telnet_running = 0; + +static hybbx_result_t telnet_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static hybbx_result_t telnet_send_byte(int fd, char ch) +{ + ssize_t sent; + const char *p = &ch; + + sent = send(fd, p, 1, MSG_NOSIGNAL); + if (sent < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_write(hybbx_session_t *session, + const char *data, size_t len) +{ + telnet_client_t *client; + size_t i; + hybbx_result_t rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (telnet_client_t *)session->transport_data; + if (client == NULL || client->fd < 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char ch = data[i]; + + if (ch == '\n' && (i == 0 || data[i - 1] != '\r')) { + rc = telnet_send_byte(client->fd, '\r'); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + ssize_t sent = send(client->fd, &ch, 1, MSG_NOSIGNAL); + + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + } + } + + return HYBBX_OK; +} + +static void telnet_on_user_data(void *ctx, const uint8_t *data, size_t len) +{ + telnet_client_ctx_t *tctx = (telnet_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (tctx == NULL || tctx->session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(tctx->session, data, len); + if (rc != HYBBX_OK) { + tctx->last_rc = rc; + } +} + +#define TELNET_CLIENT_POLL_MS 30000 + +static void telnet_send_busy(int fd) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (fd < 0) { + return; + } + + (void)send(fd, msg, sizeof(msg) - 1u, 0); +} + +static void *telnet_client_thread(void *arg) +{ + telnet_client_t *client = (telnet_client_t *)arg; + hybbx_session_t *session = NULL; + telnet_client_ctx_t tctx; + uint8_t buf[256]; + ssize_t n; + hybbx_result_t rc; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + (void)set_socket_options(client->fd, 0); + + rc = hybbx_telnet_send_greeting(client->fd); + if (rc != HYBBX_OK) { + close(client->fd); + free(client); + return NULL; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_telnet, client, &session); + if (rc != HYBBX_OK || session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + telnet_send_busy(client->fd); + } + close(client->fd); + free(client); + return NULL; + } + + { + char remote[64]; + + if (hybbx_socket_peer_name(client->fd, remote, sizeof(remote)) == HYBBX_OK) { + (void)hybbx_session_set_remote(session, remote); + } + } + + hybbx_telnet_parser_init(&client->parser); + tctx.session = session; + tctx.last_rc = HYBBX_OK; + + while (g_telnet_running) { + struct pollfd pfd; + int pr; + + pfd.fd = client->fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pr = poll(&pfd, 1, TELNET_CLIENT_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + rc = hybbx_session_tick(session); + if (rc == HYBBX_SESSION_END) { + tctx.last_rc = HYBBX_SESSION_END; + break; + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + n = recv(client->fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + rc = hybbx_telnet_parser_feed(&client->parser, client->fd, buf, (size_t)n, + telnet_on_user_data, &tctx); + if (rc != HYBBX_OK) { + break; + } + + if (tctx.last_rc == HYBBX_SESSION_END) { + break; + } + } + + hybbx_session_close(session); + shutdown(client->fd, SHUT_RDWR); + close(client->fd); + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + telnet_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + client->fd = client_fd; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, telnet_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *telnet_accept_thread(void *arg) +{ + (void)arg; + + while (g_telnet_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t telnet_init(hybbx_service_t *service) +{ + g_service = service; + return HYBBX_OK; +} + +static void telnet_shutdown(void) +{ + telnet_stop(); +} + +static hybbx_result_t telnet_start(const char *config) +{ + hybbx_result_t rc; + + if (g_telnet_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_telnet_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, g_config.port); + if (g_listen_v4 < 0) { + hybbx_socket_log_bind_failure("telnet", g_config.bind_v4, + g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[telnet] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + g_telnet_running = 1; + + if (pthread_create(&g_accept_thread, NULL, telnet_accept_thread, NULL) != 0) { + g_telnet_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[256]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[telnet] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_stop(void) +{ + if (!g_telnet_running) { + return HYBBX_OK; + } + + g_telnet_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_log_info("[telnet] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_telnet = { + .name = "telnet", + .kind = HYBBX_TRANSPORT_TELNET, + .version = 1, + .init = telnet_init, + .shutdown = telnet_shutdown, + .start = telnet_start, + .stop = telnet_stop, + .write = telnet_write, +}; diff --git a/plugins/telnet/telnet_config.c b/plugins/telnet/telnet_config.c new file mode 100644 index 0000000..d154f88 --- /dev/null +++ b/plugins/telnet/telnet_config.c @@ -0,0 +1,125 @@ +#include "hybbx/telnet.h" +#include "hybbx/util.h" + +#include +#include + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_TELNET_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_TELNET_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_telnet_config_defaults(hybbx_telnet_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_TELNET_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_TELNET_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + config->port = HYBBX_TELNET_DEFAULT_PORT; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_telnet_config_parse(const char *config, + hybbx_telnet_config_t *out) +{ + char scratch[HYBBX_TELNET_BIND_V6_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_telnet_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/telnet/telnet_proto.c b/plugins/telnet/telnet_proto.c new file mode 100644 index 0000000..523c68c --- /dev/null +++ b/plugins/telnet/telnet_proto.c @@ -0,0 +1,213 @@ +#include "telnet_proto.h" + +#include "hybbx/socket.h" + +#include +#include +#include + +#define TELNET_IAC 255 +#define TELNET_DONT 254 +#define TELNET_DO 253 +#define TELNET_WONT 252 +#define TELNET_WILL 251 +#define TELNET_SB 250 +#define TELNET_GA 249 +#define TELNET_EL 248 +#define TELNET_EC 247 +#define TELNET_AYT 246 +#define TELNET_AO 245 +#define TELNET_IP 244 +#define TELNET_BRK 243 +#define TELNET_DM 242 +#define TELNET_NOP 241 +#define TELNET_SE 240 + +#define TELNET_OPT_ECHO 1 +#define TELNET_OPT_SGA 3 +#define TELNET_OPT_TTYPE 24 +#define TELNET_OPT_NAWS 31 +#define TELNET_OPT_LINEMODE 34 +#define TELNET_OPT_CHARSET 42 + +enum telnet_parse_state { + TS_DATA = 0, + TS_IAC, + TS_CMD, + TS_SUBNEG, + TS_SUBNEG_IAC +}; + +static hybbx_result_t telnet_send_raw(int fd, const uint8_t *data, size_t len) +{ + size_t sent_total = 0; + + while (sent_total < len) { + ssize_t sent; + + sent = send(fd, data + sent_total, len - sent_total, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + + sent_total += (size_t)sent; + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_send_option(int fd, uint8_t cmd, uint8_t option) +{ + uint8_t buf[3]; + + buf[0] = TELNET_IAC; + buf[1] = cmd; + buf[2] = option; + return telnet_send_raw(fd, buf, sizeof(buf)); +} + +static hybbx_result_t telnet_reply_do(int fd, uint8_t option) +{ + switch (option) { + case TELNET_OPT_ECHO: + return telnet_send_option(fd, TELNET_WONT, option); + case TELNET_OPT_SGA: + return telnet_send_option(fd, TELNET_WILL, option); + case TELNET_OPT_TTYPE: + case TELNET_OPT_NAWS: + case TELNET_OPT_LINEMODE: + case TELNET_OPT_CHARSET: + return telnet_send_option(fd, TELNET_WONT, option); + default: + return telnet_send_option(fd, TELNET_WONT, option); + } +} + +static hybbx_result_t telnet_reply_will(int fd, uint8_t option) +{ + switch (option) { + case TELNET_OPT_SGA: + return telnet_send_option(fd, TELNET_DO, option); + case TELNET_OPT_ECHO: + return telnet_send_option(fd, TELNET_DONT, option); + default: + return telnet_send_option(fd, TELNET_DONT, option); + } +} + +static void emit_byte(hybbx_telnet_data_cb on_data, void *ctx, uint8_t byte) +{ + if (on_data != NULL) { + on_data(ctx, &byte, 1); + } +} + +void hybbx_telnet_parser_init(hybbx_telnet_parser_t *parser) +{ + if (parser == NULL) { + return; + } + + parser->state = TS_DATA; + parser->command = 0; +} + +hybbx_result_t hybbx_telnet_send_greeting(int fd) +{ + static const uint8_t greeting[] = { + TELNET_IAC, TELNET_WONT, TELNET_OPT_ECHO, + TELNET_IAC, TELNET_WILL, TELNET_OPT_SGA, + TELNET_IAC, TELNET_DONT, TELNET_OPT_LINEMODE, + }; + + if (fd < 0) { + return HYBBX_ERR_INVALID; + } + + return telnet_send_raw(fd, greeting, sizeof(greeting)); +} + +hybbx_result_t hybbx_telnet_parser_feed(hybbx_telnet_parser_t *parser, int fd, + const uint8_t *data, size_t len, + hybbx_telnet_data_cb on_data, + void *ctx) +{ + size_t i; + hybbx_result_t rc; + + if (parser == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + uint8_t byte = data[i]; + + switch (parser->state) { + case TS_DATA: + if (byte == TELNET_IAC) { + parser->state = TS_IAC; + } else { + emit_byte(on_data, ctx, byte); + } + break; + + case TS_IAC: + if (byte == TELNET_IAC) { + emit_byte(on_data, ctx, TELNET_IAC); + parser->state = TS_DATA; + } else if (byte == TELNET_WILL || byte == TELNET_WONT || + byte == TELNET_DO || byte == TELNET_DONT) { + parser->command = (int)byte; + parser->state = TS_CMD; + } else if (byte == TELNET_SB) { + parser->state = TS_SUBNEG; + } else { + parser->state = TS_DATA; + } + break; + + case TS_CMD: + if (fd >= 0) { + if (parser->command == TELNET_DO) { + rc = telnet_reply_do(fd, byte); + if (rc != HYBBX_OK) { + return rc; + } + } else if (parser->command == TELNET_WILL) { + rc = telnet_reply_will(fd, byte); + if (rc != HYBBX_OK) { + return rc; + } + } + } + parser->state = TS_DATA; + break; + + case TS_SUBNEG: + if (byte == TELNET_IAC) { + parser->state = TS_SUBNEG_IAC; + } + break; + + case TS_SUBNEG_IAC: + if (byte == TELNET_SE) { + parser->state = TS_DATA; + } else { + parser->state = TS_SUBNEG; + } + break; + + default: + parser->state = TS_DATA; + break; + } + } + + return HYBBX_OK; +} diff --git a/plugins/telnet/telnet_proto.h b/plugins/telnet/telnet_proto.h new file mode 100644 index 0000000..aa29122 --- /dev/null +++ b/plugins/telnet/telnet_proto.h @@ -0,0 +1,37 @@ +#ifndef HYBBX_TELNET_PROTO_H +#define HYBBX_TELNET_PROTO_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_telnet_parser { + int state; + int command; +} hybbx_telnet_parser_t; + +typedef void (*hybbx_telnet_data_cb)(void *ctx, const uint8_t *data, size_t len); + +void hybbx_telnet_parser_init(hybbx_telnet_parser_t *parser); + +/** Send initial option negotiation (echo, suppress go ahead). */ +hybbx_result_t hybbx_telnet_send_greeting(int fd); + +/** + * Strip telnet protocol bytes and invoke @p on_data for user payload. + */ +hybbx_result_t hybbx_telnet_parser_feed(hybbx_telnet_parser_t *parser, int fd, + const uint8_t *data, size_t len, + hybbx_telnet_data_cb on_data, + void *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TELNET_PROTO_H */ diff --git a/plugins/websocket/CMakeLists.txt b/plugins/websocket/CMakeLists.txt new file mode 100644 index 0000000..a63f83d --- /dev/null +++ b/plugins/websocket/CMakeLists.txt @@ -0,0 +1,37 @@ +add_library(hybbx_plugin_websocket STATIC + websocket.c + ws_config.c + ws_proto.c + ws_sha1.c + ws_tls.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_websocket + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_websocket + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(OpenSSL QUIET) +if(OpenSSL_FOUND) + target_compile_definitions(hybbx_plugin_websocket PRIVATE HYBBX_WS_HAVE_TLS) + target_link_libraries(hybbx_plugin_websocket PRIVATE OpenSSL::SSL OpenSSL::Crypto) + message(STATUS "WebSocket plugin: OpenSSL found — wss + self-signed certs") +else() + message(STATUS "WebSocket plugin: OpenSSL not found — plain ws only") +endif() + +target_link_libraries(hybbx_plugin_websocket PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_websocket HYBBX_HAVE_PLUGIN_WEBSOCKET) +hybbx_apply_hardening(hybbx_plugin_websocket) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_websocket + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/websocket/websocket.c b/plugins/websocket/websocket.c new file mode 100644 index 0000000..4ff2230 --- /dev/null +++ b/plugins/websocket/websocket.c @@ -0,0 +1,723 @@ +/* + * websocket — RFC6455 forward-proxy transport. INI: [transport.websocket]. + * + * Raw byte stream to the session core (no HyBBX auth at the wire layer). + * Intended behind a TLS reverse proxy; see docs/WEBSOCKET.md. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/websocket.h" +#include "hybbx/util.h" +#include "ws_proto.h" +#include "ws_tls.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define WS_CLIENT_POLL_MS 5000 +#define WS_CLIENT_PING_IDLE_SEC 20 + +typedef struct ws_client { + hybbx_ws_connection_t ws; + hybbx_session_t *hbx_session; + hybbx_result_t last_rc; + int slot_held; + time_t last_io_at; + uint8_t tx_buf[HYBBX_WS_FRAME_PAYLOAD_MAX]; + size_t tx_len; +} ws_client_t; + +typedef struct ws_client_ctx { + ws_client_t *client; +} ws_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_websocket; + +static hybbx_service_t *g_service; +static hybbx_websocket_config_t g_config; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_ws_running = 0; +static unsigned g_active_clients; +static pthread_mutex_t g_client_lock = PTHREAD_MUTEX_INITIALIZER; + +static hybbx_result_t ws_plugin_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static void log_bind_failure(const char *bind_addr, unsigned port) +{ + int err = errno; + + hybbx_socket_log_bind_failure("websocket", bind_addr, port); + if (err == EACCES && port < 1024u) { + hybbx_log_warn("[websocket] port %u is privileged — run as root, grant " + "bind permission, or set port >= 1024 in hybbx.ini", + port); + } +} + +static size_t utf8_char_span(const uint8_t *buf, size_t len, size_t pos) +{ + uint8_t b; + size_t n; + size_t i; + + if (pos >= len) { + return 0; + } + + b = buf[pos]; + if (b < 0x80u) { + return 1; + } + if ((b & 0xE0u) == 0xC0u) { + n = 2; + } else if ((b & 0xF0u) == 0xE0u) { + n = 3; + } else if ((b & 0xF8u) == 0xF0u) { + n = 4; + } else { + return 1; + } + + if (pos + n > len) { + return 0; + } + + for (i = 1; i < n; i++) { + if ((buf[pos + i] & 0xC0u) != 0x80u) { + return 1; + } + } + + return n; +} + +static size_t utf8_complete_prefix(const uint8_t *buf, size_t len) +{ + size_t pos = 0; + + while (pos < len) { + size_t span = utf8_char_span(buf, len, pos); + + if (span == 0) { + break; + } + pos += span; + } + + return pos; +} + +static hybbx_result_t ws_tx_flush(ws_client_t *client, int force_tail) +{ + size_t n; + hybbx_result_t rc; + + if (client == NULL || client->tx_len == 0) { + return HYBBX_OK; + } + + n = utf8_complete_prefix(client->tx_buf, client->tx_len); + if (n == 0 && force_tail) { + client->tx_buf[0] = '?'; + n = 1; + } + if (n == 0) { + return HYBBX_OK; + } + + rc = hybbx_ws_write_text(&client->ws, (const char *)client->tx_buf, n); + if (rc != HYBBX_OK) { + return rc; + } + client->last_io_at = time(NULL); + + if (n < client->tx_len) { + memmove(client->tx_buf, client->tx_buf + n, client->tx_len - n); + } + client->tx_len -= n; + + return HYBBX_OK; +} + +static hybbx_result_t ws_plugin_write(hybbx_session_t *session, + const char *data, size_t len) +{ + ws_client_t *client; + size_t i; + hybbx_result_t rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (ws_client_t *)session->transport_data; + if (client == NULL || !client->ws.established) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char ch = data[i]; + + if (ch == '\n' && (i == 0 || data[i - 1] != '\r')) { + if (client->tx_len + 2 > sizeof(client->tx_buf)) { + rc = ws_tx_flush(client, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + client->tx_buf[client->tx_len++] = '\r'; + } + + if (client->tx_len >= sizeof(client->tx_buf)) { + rc = ws_tx_flush(client, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + + client->tx_buf[client->tx_len++] = (uint8_t)ch; + + rc = ws_tx_flush(client, 0); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static void ws_send_busy(ws_client_t *client) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (client == NULL) { + return; + } + + (void)hybbx_ws_write_text(&client->ws, msg, sizeof(msg) - 1u); +} + +static void ws_send_limit(ws_client_t *client) +{ + static const char msg[] = + "All WebSocket connections in use. Try later.\r\n"; + + if (client == NULL) { + return; + } + + (void)hybbx_ws_write_text(&client->ws, msg, sizeof(msg) - 1u); +} + +static int ws_client_acquire_slot(ws_client_t *client) +{ + if (client == NULL) { + return 0; + } + + pthread_mutex_lock(&g_client_lock); + if (g_active_clients >= g_config.max_connections) { + pthread_mutex_unlock(&g_client_lock); + return 0; + } + + g_active_clients++; + client->slot_held = 1; + pthread_mutex_unlock(&g_client_lock); + return 1; +} + +static void ws_client_release_slot(ws_client_t *client) +{ + if (client == NULL || !client->slot_held) { + return; + } + + pthread_mutex_lock(&g_client_lock); + if (g_active_clients > 0) { + g_active_clients--; + } + client->slot_held = 0; + pthread_mutex_unlock(&g_client_lock); +} + +static void ws_on_user_data(void *ctx, const uint8_t *data, size_t len) +{ + ws_client_ctx_t *wctx = (ws_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (wctx == NULL || wctx->client == NULL || + wctx->client->hbx_session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(wctx->client->hbx_session, data, len); + if (rc != HYBBX_OK) { + wctx->client->last_rc = rc; + } +} + +static void *ws_client_thread(void *arg) +{ + ws_client_t *client = (ws_client_t *)arg; + ws_client_ctx_t wctx; + hybbx_result_t rc; + char remote[64]; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + remote[0] = '\0'; + (void)hybbx_socket_peer_name(client->ws.fd, remote, sizeof(remote)); + + if (hybbx_ws_tls_server_enabled()) { + void *tls = hybbx_ws_tls_accept(client->ws.fd); + + if (tls == NULL) { + goto cleanup; + } + hybbx_ws_connection_set_tls(&client->ws, tls); + } + + rc = hybbx_ws_server_handshake(&client->ws, g_config.path); + if (rc != HYBBX_OK) { + goto cleanup; + } + + if (!ws_client_acquire_slot(client)) { + ws_send_limit(client); + goto cleanup; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_websocket, client, + &client->hbx_session); + if (rc != HYBBX_OK || client->hbx_session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + ws_send_busy(client); + } + goto cleanup; + } + + if (remote[0] != '\0') { + (void)hybbx_session_set_remote(client->hbx_session, remote); + } + + wctx.client = client; + client->last_rc = HYBBX_OK; + client->last_io_at = time(NULL); + + while (g_ws_running && client->ws.established) { + struct pollfd pfd; + int pr; + + pfd.fd = client->ws.fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pr = poll(&pfd, 1, WS_CLIENT_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + time_t now = time(NULL); + + if (now != (time_t)-1 && + (client->last_io_at == 0 || + now - client->last_io_at >= WS_CLIENT_PING_IDLE_SEC)) { + rc = hybbx_ws_ping(&client->ws); + if (rc != HYBBX_OK) { + break; + } + client->last_io_at = now; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + rc = hybbx_ws_read_frames(&client->ws, ws_on_user_data, &wctx); + if (rc != HYBBX_OK) { + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + break; + } + client->last_io_at = time(NULL); + + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + +cleanup: + if (client->hbx_session != NULL) { + hybbx_session_close(client->hbx_session); + client->hbx_session = NULL; + } + + ws_client_release_slot(client); + + (void)ws_tx_flush(client, 1); + + if (client->ws.established) { + (void)hybbx_ws_close(&client->ws); + } + + hybbx_ws_connection_cleanup(&client->ws); + + if (client->ws.fd >= 0) { + shutdown(client->ws.fd, SHUT_RDWR); + close(client->ws.fd); + client->ws.fd = -1; + } + + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + ws_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + hybbx_ws_connection_init(&client->ws, client_fd); + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, ws_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *ws_accept_thread(void *arg) +{ + (void)arg; + + while (g_ws_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t ws_plugin_init(hybbx_service_t *service) +{ + g_service = service; + return HYBBX_OK; +} + +static void ws_plugin_shutdown(void) +{ + ws_plugin_stop(); +} + +static hybbx_result_t ws_plugin_start(const char *config) +{ + hybbx_result_t rc; + + if (g_ws_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_websocket_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, + g_config.port); + if (g_listen_v4 < 0) { + log_bind_failure(g_config.bind_v4, g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, + g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[websocket] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_ws_tls_compiled()) { + char cert_dir[HYBBX_PATH_MAX]; + hybbx_result_t tls_rc; + + if (hybbx_path_resolve(cert_dir, sizeof(cert_dir), + g_config.cert_dir) != HYBBX_OK) { + hybbx_strlcpy(cert_dir, g_config.cert_dir, sizeof(cert_dir)); + } + + tls_rc = hybbx_ws_tls_ensure_certs(cert_dir); + + if (tls_rc == HYBBX_OK) { + tls_rc = hybbx_ws_tls_server_start(cert_dir); + } + if (tls_rc != HYBBX_OK) { + hybbx_log_warn("[websocket] TLS init failed (%d), plain ws only", + (int)tls_rc); + } + } + + g_ws_running = 1; + g_active_clients = 0; + + if (pthread_create(&g_accept_thread, NULL, ws_accept_thread, NULL) != 0) { + g_ws_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[384]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[websocket] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " path=%s max_connections=%u (%s, forward-proxy)", + g_config.path, g_config.max_connections, + hybbx_ws_tls_server_enabled() ? "wss" : "ws"); + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t ws_plugin_stop(void) +{ + if (!g_ws_running) { + return HYBBX_OK; + } + + g_ws_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_ws_tls_server_stop(); + hybbx_log_info("[websocket] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_websocket = { + .name = "websocket", + .kind = HYBBX_TRANSPORT_WEBSOCKET, + .version = 1, + .init = ws_plugin_init, + .shutdown = ws_plugin_shutdown, + .start = ws_plugin_start, + .stop = ws_plugin_stop, + .write = ws_plugin_write, +}; diff --git a/plugins/websocket/ws_config.c b/plugins/websocket/ws_config.c new file mode 100644 index 0000000..baf88b2 --- /dev/null +++ b/plugins/websocket/ws_config.c @@ -0,0 +1,163 @@ +#include "hybbx/websocket.h" +#include "hybbx/util.h" + +#include +#include + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_WEBSOCKET_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_WEBSOCKET_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static unsigned int parse_max_connections(const char *value) +{ + char *end; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + } + + n = strtoul(value, &end, 10); + if (end == value || *end != '\0' || n == 0 || n > 65535u) { + return HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + } + + return (unsigned int)n; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_websocket_config_defaults(hybbx_websocket_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_WEBSOCKET_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_WEBSOCKET_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + hybbx_strlcpy(config->path, HYBBX_WEBSOCKET_DEFAULT_PATH, + sizeof(config->path)); + hybbx_strlcpy(config->cert_dir, HYBBX_WEBSOCKET_DEFAULT_CERT_DIR, + sizeof(config->cert_dir)); + config->port = HYBBX_WEBSOCKET_DEFAULT_PORT; + config->max_connections = HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_websocket_config_parse(const char *config, + hybbx_websocket_config_t *out) +{ + char scratch[HYBBX_WEBSOCKET_PATH_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_websocket_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "max_connections", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "max_online", scratch, sizeof(scratch)); + } + out->max_connections = parse_max_connections(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "path", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->path, value, sizeof(out->path)); + } + + value = find_kv(config, "cert_dir", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->cert_dir, value, sizeof(out->cert_dir)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/websocket/ws_proto.c b/plugins/websocket/ws_proto.c new file mode 100644 index 0000000..b0e63b5 --- /dev/null +++ b/plugins/websocket/ws_proto.c @@ -0,0 +1,565 @@ +#include "ws_proto.h" +#include "ws_sha1.h" +#include "ws_tls.h" + +#include +#include +#include +#include +#include +#include +#include + +#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +static int header_has_token(const char *value, const char *token) +{ + const char *p; + + if (value == NULL || token == NULL) { + return 0; + } + + for (p = value; *p != '\0'; p++) { + if (strncasecmp(p, token, strlen(token)) == 0) { + return 1; + } + } + + return 0; +} + +static int header_value_equals(const char *value, const char *expected) +{ + size_t expected_len; + + if (value == NULL || expected == NULL) { + return 0; + } + + expected_len = strlen(expected); + if (strncasecmp(value, expected, expected_len) != 0) { + return 0; + } + + { + char c = value[expected_len]; + + return c == '\0' || c == '\r' || c == ' ' || c == '\t' || c == ','; + } +} + +static ssize_t recv_some(int fd, void *buf, size_t len) +{ + ssize_t n; + + n = recv(fd, buf, len, 0); + if (n < 0 && errno == EINTR) { + return 0; + } + return n; +} + +static ssize_t ws_recv_some(hybbx_ws_connection_t *ws, void *buf, size_t len) +{ + if (ws == NULL) { + return -1; + } + + if (ws->tls != NULL) { + return hybbx_ws_tls_recv(ws->tls, buf, len); + } + + return recv_some(ws->fd, buf, len); +} + +static ssize_t send_all(int fd, const void *buf, size_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + size_t sent = 0; + + while (sent < len) { + ssize_t n = send(fd, p + sent, len - sent, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (n == 0) { + return -1; + } + sent += (size_t)n; + } + + return (ssize_t)sent; +} + +static ssize_t ws_send_all(hybbx_ws_connection_t *ws, const void *buf, + size_t len) +{ + if (ws == NULL) { + return -1; + } + + if (ws->tls != NULL) { + return hybbx_ws_tls_send(ws->tls, buf, len); + } + + return send_all(ws->fd, buf, len); +} + +static int base64_encode(const uint8_t *in, size_t in_len, char *out, + size_t out_len) +{ + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i; + size_t o = 0; + + if (out_len < ((in_len + 2) / 3) * 4 + 1) { + return -1; + } + + for (i = 0; i < in_len; i += 3) { + uint32_t v = (uint32_t)in[i] << 16; + + if (i + 1 < in_len) { + v |= (uint32_t)in[i + 1] << 8; + } + if (i + 2 < in_len) { + v |= (uint32_t)in[i + 2]; + } + + out[o++] = tbl[(v >> 18) & 63u]; + out[o++] = tbl[(v >> 12) & 63u]; + out[o++] = (i + 1 < in_len) ? tbl[(v >> 6) & 63u] : '='; + out[o++] = (i + 2 < in_len) ? tbl[v & 63u] : '='; + } + + out[o] = '\0'; + return 0; +} + +static const char *header_value(const char *headers, const char *name) +{ + size_t name_len = strlen(name); + const char *p = headers; + + while (p != NULL && *p != '\0') { + const char *line_end = strstr(p, "\r\n"); + + if (line_end == NULL) { + break; + } + + if ((size_t)(line_end - p) > name_len + 2 && + strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') { + const char *value = p + name_len + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + return value; + } + + p = line_end + 2; + } + + return NULL; +} + +static int path_matches(const char *request_line, const char *path) +{ + const char *sp1; + const char *sp2; + size_t path_len; + + if (path == NULL || path[0] == '\0') { + return 1; + } + + sp1 = strchr(request_line, ' '); + if (sp1 == NULL) { + return 0; + } + sp1++; + sp2 = strchr(sp1, ' '); + if (sp2 == NULL) { + return 0; + } + + path_len = strlen(path); + if ((size_t)(sp2 - sp1) < path_len) { + return 0; + } + if (strncmp(sp1, path, path_len) != 0) { + return 0; + } + if (sp1[path_len] != ' ' && sp1[path_len] != '?' && + sp1[path_len] != '\0') { + return 0; + } + + return 1; +} + +void hybbx_ws_connection_init(hybbx_ws_connection_t *ws, int fd) +{ + if (ws == NULL) { + return; + } + + memset(ws, 0, sizeof(*ws)); + ws->fd = fd; +} + +void hybbx_ws_connection_set_tls(hybbx_ws_connection_t *ws, void *tls) +{ + if (ws == NULL) { + return; + } + + ws->tls = tls; +} + +void hybbx_ws_connection_cleanup(hybbx_ws_connection_t *ws) +{ + if (ws == NULL) { + return; + } + + if (ws->tls != NULL) { + hybbx_ws_tls_shutdown(ws->tls); + hybbx_ws_tls_free(ws->tls); + ws->tls = NULL; + } +} + +hybbx_result_t hybbx_ws_server_handshake(hybbx_ws_connection_t *ws, + const char *path) +{ + char buf[HYBBX_WS_HANDSHAKE_MAX]; + size_t total = 0; + const char *key_hdr; + char key[128]; + char accept_src[256]; + uint8_t digest[20]; + char accept_b64[64]; + char response[512]; + const char *upgrade; + const char *connection; + + if (ws == NULL || ws->fd < 0) { + return HYBBX_ERR_INVALID; + } + + while (total + 1 < sizeof(buf)) { + ssize_t n = ws_recv_some(ws, buf + total, sizeof(buf) - 1 - total); + + if (n < 0) { + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + + total += (size_t)n; + buf[total] = '\0'; + + if (strstr(buf, "\r\n\r\n") != NULL) { + break; + } + } + + if (strncmp(buf, "GET ", 4) != 0) { + return HYBBX_ERR_DENIED; + } + + { + const char *eol = strstr(buf, "\r\n"); + char first_line[256]; + size_t line_len; + + if (eol == NULL) { + return HYBBX_ERR_DENIED; + } + line_len = (size_t)(eol - buf); + if (line_len >= sizeof(first_line)) { + return HYBBX_ERR_DENIED; + } + memcpy(first_line, buf, line_len); + first_line[line_len] = '\0'; + if (!path_matches(first_line, path)) { + return HYBBX_ERR_DENIED; + } + } + + upgrade = header_value(buf, "Upgrade"); + connection = header_value(buf, "Connection"); + if (upgrade == NULL || connection == NULL || + !header_value_equals(upgrade, "websocket") || + !header_has_token(connection, "upgrade")) { + return HYBBX_ERR_DENIED; + } + + key_hdr = header_value(buf, "Sec-WebSocket-Key"); + if (key_hdr == NULL) { + return HYBBX_ERR_DENIED; + } + + { + const char *key_end = strstr(key_hdr, "\r\n"); + size_t key_len; + + if (key_end == NULL) { + return HYBBX_ERR_DENIED; + } + key_len = (size_t)(key_end - key_hdr); + while (key_len > 0 && + (key_hdr[key_len - 1] == ' ' || key_hdr[key_len - 1] == '\t')) { + key_len--; + } + if (key_len >= sizeof(key)) { + return HYBBX_ERR_DENIED; + } + memcpy(key, key_hdr, key_len); + key[key_len] = '\0'; + } + + snprintf(accept_src, sizeof(accept_src), "%s%s", key, WS_GUID); + hybbx_ws_sha1((const uint8_t *)accept_src, strlen(accept_src), digest); + if (base64_encode(digest, sizeof(digest), accept_b64, + sizeof(accept_b64)) != 0) { + return HYBBX_ERR_IO; + } + + snprintf(response, sizeof(response), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "\r\n", + accept_b64); + + if (ws_send_all(ws, response, strlen(response)) < 0) { + return HYBBX_ERR_IO; + } + + ws->established = 1; + ws->rx_len = 0; + return HYBBX_OK; +} + +static hybbx_result_t ws_handle_frame(hybbx_ws_connection_t *ws, + const uint8_t *frame, size_t frame_len, + hybbx_ws_data_cb on_data, void *ctx) +{ + uint8_t opcode; + uint64_t payload_len; + size_t header_len; + uint8_t mask[4]; + uint8_t payload[HYBBX_WS_FRAME_PAYLOAD_MAX]; + size_t i; + + if (frame_len < 2) { + return HYBBX_ERR_IO; + } + + opcode = frame[0] & 0x0Fu; + if ((frame[0] & 0x80) == 0) { + return HYBBX_ERR_UNSUPPORTED; + } + + if ((frame[1] & 0x80) == 0) { + return HYBBX_ERR_IO; + } + + payload_len = frame[1] & 0x7Fu; + header_len = 2; + + if (payload_len == 126) { + if (frame_len < 4) { + return HYBBX_ERR_IO; + } + payload_len = ((uint64_t)frame[2] << 8) | frame[3]; + header_len = 4; + } else if (payload_len == 127) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (payload_len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (frame_len < header_len + 4 + payload_len) { + return HYBBX_ERR_IO; + } + + memcpy(mask, frame + header_len, 4); + header_len += 4; + + for (i = 0; i < payload_len; i++) { + payload[i] = frame[header_len + i] ^ mask[i % 4]; + } + + switch (opcode) { + case 0x1: + case 0x2: + if (on_data != NULL && payload_len > 0) { + on_data(ctx, payload, (size_t)payload_len); + } + return HYBBX_OK; + case 0x8: + return HYBBX_ERR_IO; + case 0x9: + { + uint8_t pong[2]; + + pong[0] = 0x8A; + pong[1] = 0x00; + (void)ws_send_all(ws, pong, 2); + } + return HYBBX_OK; + case 0xA: + return HYBBX_OK; + default: + return HYBBX_OK; + } +} + +hybbx_result_t hybbx_ws_read_frames(hybbx_ws_connection_t *ws, + hybbx_ws_data_cb on_data, + void *ctx) +{ + uint8_t tmp[256]; + ssize_t n; + + if (ws == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + n = ws_recv_some(ws, tmp, sizeof(tmp)); + if (n < 0) { + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + + if (ws->rx_len + (size_t)n > sizeof(ws->rx_buf)) { + ws->rx_len = 0; + return HYBBX_ERR_IO; + } + + memcpy(ws->rx_buf + ws->rx_len, tmp, (size_t)n); + ws->rx_len += (size_t)n; + + while (ws->rx_len >= 2) { + uint64_t payload_len = ws->rx_buf[1] & 0x7Fu; + size_t header_len = 2; + size_t frame_len; + hybbx_result_t rc; + + if (payload_len == 126) { + if (ws->rx_len < 4) { + return HYBBX_OK; + } + payload_len = ((uint64_t)ws->rx_buf[2] << 8) | ws->rx_buf[3]; + header_len = 4; + } else if (payload_len == 127) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (payload_len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_UNSUPPORTED; + } + + frame_len = header_len + 4 + (size_t)payload_len; + if (ws->rx_len < frame_len) { + return HYBBX_OK; + } + + rc = ws_handle_frame(ws, ws->rx_buf, frame_len, on_data, ctx); + if (rc != HYBBX_OK) { + return rc; + } + + if (ws->rx_len > frame_len) { + memmove(ws->rx_buf, ws->rx_buf + frame_len, + ws->rx_len - frame_len); + } + ws->rx_len -= frame_len; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_write_text(hybbx_ws_connection_t *ws, + const char *data, size_t len) +{ + uint8_t hdr[10]; + size_t hdr_len = 2; + + if (ws == NULL || data == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + if (len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_INVALID; + } + + hdr[0] = 0x81; + if (len < 126) { + hdr[1] = (uint8_t)len; + hdr_len = 2; + } else { + hdr[1] = 126; + hdr[2] = (uint8_t)((len >> 8) & 0xFFu); + hdr[3] = (uint8_t)(len & 0xFFu); + hdr_len = 4; + } + + if (ws_send_all(ws, hdr, hdr_len) < 0) { + return HYBBX_ERR_IO; + } + if (len > 0 && ws_send_all(ws, data, len) < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_ping(hybbx_ws_connection_t *ws) +{ + uint8_t frame[2]; + + if (ws == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 0x89; + frame[1] = 0x00; + + if (ws_send_all(ws, frame, sizeof(frame)) < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_close(hybbx_ws_connection_t *ws) +{ + uint8_t frame[2]; + + if (ws == NULL || ws->fd < 0) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 0x88; + frame[1] = 0x00; + (void)ws_send_all(ws, frame, 2); + ws->established = 0; + return HYBBX_OK; +} diff --git a/plugins/websocket/ws_proto.h b/plugins/websocket/ws_proto.h new file mode 100644 index 0000000..5a1cb23 --- /dev/null +++ b/plugins/websocket/ws_proto.h @@ -0,0 +1,63 @@ +#ifndef HYBBX_WS_PROTO_H +#define HYBBX_WS_PROTO_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_WS_HANDSHAKE_MAX 4096u +#define HYBBX_WS_FRAME_PAYLOAD_MAX 4096u + +typedef void (*hybbx_ws_data_cb)(void *ctx, const uint8_t *data, size_t len); + +typedef struct hybbx_ws_connection { + int fd; + void *tls; + int established; + uint8_t rx_buf[HYBBX_WS_FRAME_PAYLOAD_MAX + 16]; + size_t rx_len; +} hybbx_ws_connection_t; + +void hybbx_ws_connection_init(hybbx_ws_connection_t *ws, int fd); + +/** Attach TLS session (opaque SSL*) after accept; plain ws when NULL. */ +void hybbx_ws_connection_set_tls(hybbx_ws_connection_t *ws, void *tls); + +/** Release TLS resources; does not close @c fd. */ +void hybbx_ws_connection_cleanup(hybbx_ws_connection_t *ws); + +/** + * Perform server-side HTTP Upgrade handshake. + * @p path must match the request URI path (e.g. /hybbx) when non-empty. + */ +hybbx_result_t hybbx_ws_server_handshake(hybbx_ws_connection_t *ws, + const char *path); + +/** + * Read WebSocket frames; invoke @p on_data for each complete text/binary + * payload from the client. Returns HYBBX_ERR_IO on disconnect. + */ +hybbx_result_t hybbx_ws_read_frames(hybbx_ws_connection_t *ws, + hybbx_ws_data_cb on_data, + void *ctx); + +/** Send one server text frame (unmasked). */ +hybbx_result_t hybbx_ws_write_text(hybbx_ws_connection_t *ws, + const char *data, size_t len); + +/** Send one server ping frame (unmasked, empty payload). */ +hybbx_result_t hybbx_ws_ping(hybbx_ws_connection_t *ws); + +/** Send WebSocket close frame. */ +hybbx_result_t hybbx_ws_close(hybbx_ws_connection_t *ws); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WS_PROTO_H */ diff --git a/plugins/websocket/ws_sha1.c b/plugins/websocket/ws_sha1.c new file mode 100644 index 0000000..53ad4c0 --- /dev/null +++ b/plugins/websocket/ws_sha1.c @@ -0,0 +1,138 @@ +/* + * Minimal SHA-1 for WebSocket handshake (RFC 6455). Public-domain style. + */ +#include "ws_sha1.h" + +#include +#include +#include + +typedef struct ws_sha1_ctx { + uint32_t state[5]; + uint64_t count; + uint8_t buffer[64]; +} ws_sha1_ctx_t; + +static uint32_t rol(uint32_t v, unsigned bits) +{ + return (v << bits) | (v >> (32u - bits)); +} + +static void ws_sha1_transform(ws_sha1_ctx_t *ctx, const uint8_t block[64]) +{ + uint32_t w[80]; + uint32_t a, b, c, d, e, f, k, t; + unsigned i; + + for (i = 0; i < 16; i++) { + w[i] = ((uint32_t)block[i * 4] << 24) | + ((uint32_t)block[i * 4 + 1] << 16) | + ((uint32_t)block[i * 4 + 2] << 8) | + (uint32_t)block[i * 4 + 3]; + } + for (i = 16; i < 80; i++) { + w[i] = rol(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + + for (i = 0; i < 80; i++) { + if (i < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + t = rol(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; +} + +static void ws_sha1_init(ws_sha1_ctx_t *ctx) +{ + ctx->state[0] = 0x67452301u; + ctx->state[1] = 0xEFCDAB89u; + ctx->state[2] = 0x98BADCFEu; + ctx->state[3] = 0x10325476u; + ctx->state[4] = 0xC3D2E1F0u; + ctx->count = 0; +} + +static void ws_sha1_update(ws_sha1_ctx_t *ctx, const uint8_t *data, size_t len) +{ + size_t i = 0; + size_t idx = (size_t)((ctx->count >> 3) & 63u); + + ctx->count += (uint64_t)len * 8u; + + if (idx + len > 63) { + memcpy(ctx->buffer + idx, data, 64 - idx); + ws_sha1_transform(ctx, ctx->buffer); + for (i = 64 - idx; i + 63 < len; i += 64) { + ws_sha1_transform(ctx, data + i); + } + idx = 0; + } else { + i = 0; + } + + memcpy(ctx->buffer + idx, data + i, len - i); +} + +static void ws_sha1_final(ws_sha1_ctx_t *ctx, uint8_t out[20]) +{ + uint8_t final[64]; + size_t idx = (size_t)((ctx->count >> 3) & 63u); + unsigned i; + + memcpy(final, ctx->buffer, idx); + final[idx++] = 0x80; + if (idx > 56) { + memset(final + idx, 0, 64 - idx); + ws_sha1_transform(ctx, final); + idx = 0; + } + memset(final + idx, 0, 56 - idx); + + for (i = 0; i < 8; i++) { + final[56 + i] = (uint8_t)(ctx->count >> (56 - i * 8)); + } + ws_sha1_transform(ctx, final); + + for (i = 0; i < 5; i++) { + out[i * 4] = (uint8_t)(ctx->state[i] >> 24); + out[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 16); + out[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 8); + out[i * 4 + 3] = (uint8_t)(ctx->state[i]); + } +} + +void hybbx_ws_sha1(const uint8_t *data, size_t len, uint8_t out[20]) +{ + ws_sha1_ctx_t ctx; + + ws_sha1_init(&ctx); + ws_sha1_update(&ctx, data, len); + ws_sha1_final(&ctx, out); +} diff --git a/plugins/websocket/ws_sha1.h b/plugins/websocket/ws_sha1.h new file mode 100644 index 0000000..b685cf7 --- /dev/null +++ b/plugins/websocket/ws_sha1.h @@ -0,0 +1,9 @@ +#ifndef HYBBX_WS_SHA1_H +#define HYBBX_WS_SHA1_H + +#include +#include + +void hybbx_ws_sha1(const uint8_t *data, size_t len, uint8_t out[20]); + +#endif /* HYBBX_WS_SHA1_H */ diff --git a/plugins/websocket/ws_tls.c b/plugins/websocket/ws_tls.c new file mode 100644 index 0000000..679ce59 --- /dev/null +++ b/plugins/websocket/ws_tls.c @@ -0,0 +1,400 @@ +#include "ws_tls.h" + +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/websocket.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include + +#ifdef HYBBX_WS_HAVE_TLS + +#include +#include +#include +#include +#include + +static SSL_CTX *g_ssl_ctx; +static int g_tls_enabled; + +static hybbx_result_t mkdir_cert_dir(const char *cert_dir) +{ + char parent[HYBBX_PATH_MAX]; + struct stat st; + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (stat(cert_dir, &st) == 0) { + return S_ISDIR(st.st_mode) ? HYBBX_OK : HYBBX_ERR_IO; + } + + if (hybbx_path_dirname(cert_dir, parent, sizeof(parent)) == HYBBX_OK && + parent[0] != '\0' && strcmp(parent, cert_dir) != 0 && + stat(parent, &st) != 0) { + if (mkdir_cert_dir(parent) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + if (mkdir(cert_dir, 0700) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void build_cert_path(const char *cert_dir, const char *name, + char *out, size_t out_len) +{ + if (cert_dir == NULL || name == NULL || out == NULL || out_len == 0) { + if (out != NULL && out_len > 0) { + out[0] = '\0'; + } + return; + } + + snprintf(out, out_len, "%s/%s", cert_dir, name); +} + +static int cert_files_exist(const char *cert_path, const char *key_path) +{ + struct stat st; + + return stat(cert_path, &st) == 0 && S_ISREG(st.st_mode) && + stat(key_path, &st) == 0 && S_ISREG(st.st_mode); +} + +static hybbx_result_t generate_self_signed(const char *cert_path, + const char *key_path) +{ + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = NULL; + X509 *cert = NULL; + X509_NAME *name; + FILE *fp; + int rc = HYBBX_ERR_IO; + + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (pctx == NULL || + EVP_PKEY_keygen_init(pctx) <= 0 || + EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0 || + EVP_PKEY_keygen(pctx, &pkey) <= 0 || pkey == NULL) { + goto done; + } + + cert = X509_new(); + if (cert == NULL) { + goto done; + } + + if (X509_set_version(cert, 2) != 1 || + ASN1_INTEGER_set(X509_get_serialNumber(cert), 1) != 1 || + X509_gmtime_adj(X509_get_notBefore(cert), 0) == NULL || + X509_gmtime_adj(X509_get_notAfter(cert), + (long)HYBBX_WS_TLS_CERT_VALID_DAYS * 24L * 3600L) == + NULL || + X509_set_pubkey(cert, pkey) != 1) { + goto done; + } + + name = X509_get_subject_name(cert); + if (name == NULL || + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char *)"hybbx", -1, -1, + 0) != 1 || + X509_set_issuer_name(cert, name) != 1 || + X509_sign(cert, pkey, EVP_sha256()) <= 0) { + goto done; + } + + fp = fopen(key_path, "w"); + if (fp == NULL || + PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL) != 1) { + if (fp != NULL) { + fclose(fp); + unlink(key_path); + } + goto done; + } + fclose(fp); + (void)chmod(key_path, 0600); + + fp = fopen(cert_path, "w"); + if (fp == NULL || PEM_write_X509(fp, cert) != 1) { + if (fp != NULL) { + fclose(fp); + unlink(cert_path); + } + goto done; + } + fclose(fp); + (void)chmod(cert_path, 0644); + + rc = HYBBX_OK; + +done: + if (cert != NULL) { + X509_free(cert); + } + if (pkey != NULL) { + EVP_PKEY_free(pkey); + } + if (pctx != NULL) { + EVP_PKEY_CTX_free(pctx); + } + + return rc; +} + +int hybbx_ws_tls_compiled(void) +{ + return 1; +} + +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir) +{ + char cert_path[HYBBX_PATH_MAX]; + char key_path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = mkdir_cert_dir(cert_dir); + if (rc != HYBBX_OK) { + return rc; + } + + build_cert_path(cert_dir, HYBBX_WS_TLS_CERT_FILENAME, cert_path, + sizeof(cert_path)); + build_cert_path(cert_dir, HYBBX_WS_TLS_KEY_FILENAME, key_path, + sizeof(key_path)); + + if (cert_files_exist(cert_path, key_path)) { + return HYBBX_OK; + } + + rc = generate_self_signed(cert_path, key_path); + if (rc == HYBBX_OK) { + hybbx_log_info("[websocket] created self-signed TLS certificate in %s", + cert_dir); + } + + return rc; +} + +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir) +{ + char cert_path[HYBBX_PATH_MAX]; + char key_path[HYBBX_PATH_MAX]; + + if (g_tls_enabled) { + return HYBBX_OK; + } + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + build_cert_path(cert_dir, HYBBX_WS_TLS_CERT_FILENAME, cert_path, + sizeof(cert_path)); + build_cert_path(cert_dir, HYBBX_WS_TLS_KEY_FILENAME, key_path, + sizeof(key_path)); + + if (!cert_files_exist(cert_path, key_path)) { + return HYBBX_ERR_IO; + } + + if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | + OPENSSL_INIT_LOAD_CRYPTO_STRINGS, + NULL) != 1) { + return HYBBX_ERR_IO; + } + + g_ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (g_ssl_ctx == NULL) { + return HYBBX_ERR_IO; + } + + SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION); + + if (SSL_CTX_use_certificate_file(g_ssl_ctx, cert_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_check_private_key(g_ssl_ctx) != 1) { + SSL_CTX_free(g_ssl_ctx); + g_ssl_ctx = NULL; + return HYBBX_ERR_IO; + } + + g_tls_enabled = 1; + return HYBBX_OK; +} + +void hybbx_ws_tls_server_stop(void) +{ + if (g_ssl_ctx != NULL) { + SSL_CTX_free(g_ssl_ctx); + g_ssl_ctx = NULL; + } + g_tls_enabled = 0; +} + +int hybbx_ws_tls_server_enabled(void) +{ + return g_tls_enabled; +} + +void *hybbx_ws_tls_accept(int fd) +{ + SSL *ssl; + + if (!g_tls_enabled || g_ssl_ctx == NULL || fd < 0) { + return NULL; + } + + ssl = SSL_new(g_ssl_ctx); + if (ssl == NULL) { + return NULL; + } + + if (SSL_set_fd(ssl, fd) != 1 || SSL_accept(ssl) <= 0) { + SSL_free(ssl); + return NULL; + } + + return ssl; +} + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len) +{ + SSL *ssl = (SSL *)tls; + int n; + + if (ssl == NULL || buf == NULL || len == 0) { + return -1; + } + + do { + n = SSL_read(ssl, buf, (int)len); + } while (n < 0 && SSL_get_error(ssl, n) == SSL_ERROR_SYSCALL && + errno == EINTR); + + return (ssize_t)n; +} + +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + SSL *ssl = (SSL *)tls; + size_t sent = 0; + + if (ssl == NULL || buf == NULL) { + return -1; + } + + while (sent < len) { + int n; + + do { + n = SSL_write(ssl, p + sent, (int)(len - sent)); + } while (n < 0 && SSL_get_error(ssl, n) == SSL_ERROR_SYSCALL && + errno == EINTR); + + if (n <= 0) { + return -1; + } + sent += (size_t)n; + } + + return (ssize_t)sent; +} + +void hybbx_ws_tls_shutdown(void *tls) +{ + SSL *ssl = (SSL *)tls; + + if (ssl != NULL) { + (void)SSL_shutdown(ssl); + } +} + +void hybbx_ws_tls_free(void *tls) +{ + SSL *ssl = (SSL *)tls; + + if (ssl != NULL) { + SSL_free(ssl); + } +} + +#else /* !HYBBX_WS_HAVE_TLS */ + +int hybbx_ws_tls_compiled(void) +{ + return 0; +} + +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir) +{ + (void)cert_dir; + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir) +{ + (void)cert_dir; + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_ws_tls_server_stop(void) +{ +} + +int hybbx_ws_tls_server_enabled(void) +{ + return 0; +} + +void *hybbx_ws_tls_accept(int fd) +{ + (void)fd; + return NULL; +} + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len) +{ + (void)tls; + (void)buf; + (void)len; + return -1; +} + +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len) +{ + (void)tls; + (void)buf; + (void)len; + return -1; +} + +void hybbx_ws_tls_shutdown(void *tls) +{ + (void)tls; +} + +void hybbx_ws_tls_free(void *tls) +{ + (void)tls; +} + +#endif /* HYBBX_WS_HAVE_TLS */ diff --git a/plugins/websocket/ws_tls.h b/plugins/websocket/ws_tls.h new file mode 100644 index 0000000..2615fab --- /dev/null +++ b/plugins/websocket/ws_tls.h @@ -0,0 +1,43 @@ +#ifndef HYBBX_WS_TLS_H +#define HYBBX_WS_TLS_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Return 1 when this build links OpenSSL for WebSocket TLS. */ +int hybbx_ws_tls_compiled(void); + +/** + * Create @p cert_dir if needed and generate a self-signed certificate on first + * start when files are missing. No-op when OpenSSL is not compiled in. + */ +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir); + +/** Load server TLS context after @p cert_dir is ready. */ +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir); + +void hybbx_ws_tls_server_stop(void); + +/** Return 1 when the server accepts TLS (OpenSSL + certs loaded). */ +int hybbx_ws_tls_server_enabled(void); + +/** Perform TLS server handshake on @p fd; returns opaque SSL* or NULL. */ +void *hybbx_ws_tls_accept(int fd); + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len); +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len); + +void hybbx_ws_tls_shutdown(void *tls); +void hybbx_ws_tls_free(void *tls); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WS_TLS_H */ diff --git a/scripts/build-amiga-telnet.sh b/scripts/build-amiga-telnet.sh new file mode 100755 index 0000000..b423923 --- /dev/null +++ b/scripts/build-amiga-telnet.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +# Cross-build hybbx-telnet for AmigaOS 3.9+ (bebbo-gcc / clib2). +set -eu + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +BUILD="${1:-$ROOT/build-amiga}" +SDK="${AMIGA_SDK_PATH:-/opt/amiga}" + +shift 2>/dev/null || true + +if [ ! -x "$SDK/bin/m68k-amigaos-gcc" ]; then + echo "Amiga SDK not found at $SDK (set AMIGA_SDK_PATH)" >&2 + exit 1 +fi + +cmake -B "$BUILD" \ + -DCMAKE_TOOLCHAIN_FILE="$ROOT/cmake/Toolchain-AmigaOS.cmake" \ + -DAMIGA_SDK_PATH="$SDK" \ + -DHYBBX_CLIENTS_ONLY=ON \ + -DCMAKE_BUILD_TYPE=Release \ + "$@" + +cmake --build "$BUILD" --target hybbx-telnet + +echo "AmigaOS client:" +echo " $BUILD/src/clients/hybbx-telnet" diff --git a/scripts/build-clients.sh b/scripts/build-clients.sh new file mode 100755 index 0000000..3e909f9 --- /dev/null +++ b/scripts/build-clients.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh +# Build hybbx-telnet, hybbx-terminal, and hybbx-ssh (when libssh is available) +# without the hybbx server or plugins. +set -eu + +ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" +BUILD="${1:-$ROOT/build-clients}" + +shift 2>/dev/null || true + +cmake -B "$BUILD" -DHYBBX_CLIENTS_ONLY=ON "$@" +cmake --build "$BUILD" + +echo "Clients:" +echo " $BUILD/src/clients/hybbx-telnet" +echo " $BUILD/src/clients/hybbx-terminal" +if [ -x "$BUILD/src/clients/hybbx-ssh" ]; then + echo " $BUILD/src/clients/hybbx-ssh" +fi diff --git a/scripts/dev-setup.sh b/scripts/dev-setup.sh new file mode 100755 index 0000000..69929d3 --- /dev/null +++ b/scripts/dev-setup.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# One-shot dev environment: configure, build, IDE compile_commands symlink. +set -e + +root="$(cd "$(dirname "$0")/.." && pwd)" + +cmake -B "$root/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +cmake --build "$root/build" + +if [ -f "$root/build/compile_commands.json" ]; then + ln -sf build/compile_commands.json "$root/compile_commands.json" + echo "Linked compile_commands.json -> build/compile_commands.json" +fi + +echo "Ready. Install (default → ./bin): cmake --install $root/build" +echo "Start: $root/bin/hybbx-start or $root/scripts/hybbx.sh" diff --git a/scripts/hybbx.sh b/scripts/hybbx.sh new file mode 100755 index 0000000..9652654 --- /dev/null +++ b/scripts/hybbx.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Start hybbxd only. Build and install with CMake (see README). +# +# Default install (source tree): ./bin/hybbx-start +# Prefix install: /usr/local/hybbx/hybbx-start (or $HOME/hybbx/…) +# Dev (no install): ./scripts/hybbx.sh +# +# Detached: hybbx-start --screen | --tmux [--attach] +set -e + +self="${0#./}" +case "$self" in + /*) script_path="$self" ;; + *) script_path="$(pwd)/$self" ;; +esac +script_dir="$(cd "$(dirname "$script_path")" && pwd)" +script_name="$(basename "$script_path")" + +# Installed layout: /hybbx-start + /hybbxd (./bin or /hybbx) +if [ "$script_name" = "hybbx-start" ] && [ -x "$script_dir/hybbxd" ]; then + prefix="$script_dir" + config="${HYBBX_CONFIG:-$prefix/hybbx.ini}" + binary="$script_dir/hybbxd" + export HYBBX_ROOT="${HYBBX_ROOT:-$prefix}" + cd "$prefix" + if [ ! -f "$config" ]; then + echo "Config not found: $config" >&2 + exit 1 + fi + if [ "$#" -eq 0 ]; then + exec "$binary" -c "$config" + fi + exec "$binary" -c "$config" "$@" +fi + +# Explicit or known install roots +root="$(cd "$script_dir/.." && pwd)" +if [ -n "$HYBBX_ROOT" ]; then + prefix="$HYBBX_ROOT" +elif [ -x "$root/bin/hybbxd" ]; then + prefix="$root/bin" +elif [ -x "/usr/local/hybbx/hybbxd" ]; then + prefix="/usr/local/hybbx" +elif [ -n "$HOME" ] && [ -x "$HOME/hybbx/hybbxd" ]; then + prefix="$HOME/hybbx" +else + prefix="" +fi + +if [ -n "$prefix" ]; then + config="${HYBBX_CONFIG:-$prefix/hybbx.ini}" + binary="$prefix/hybbxd" + if [ -x "$binary" ] && [ -f "$config" ]; then + export HYBBX_ROOT="${HYBBX_ROOT:-$prefix}" + cd "$prefix" + if [ "$#" -eq 0 ]; then + exec "$binary" -c "$config" + fi + exec "$binary" -c "$config" "$@" + fi +fi + +# Development tree (repo checkout, no cmake --install yet) +config="${HYBBX_CONFIG:-$root/local/hybbx.ini}" +build="$root/build/src/hybbxd" + +if [ -f "$config" ] && [ -x "$build" ]; then + export HYBBX_ROOT="${HYBBX_ROOT:-$root}" + cd "$root" + if [ "$#" -eq 0 ]; then + exec "$build" -c "$config" + fi + exec "$build" -c "$config" "$@" +fi + +echo "HyBBX not found." >&2 +echo "Build and install (default → ./bin):" >&2 +echo " cmake -B build -DCMAKE_BUILD_TYPE=Release" >&2 +echo " cmake --build build" >&2 +echo " cmake --install build" >&2 +echo " ./bin/hybbx-start" >&2 +echo "System prefix:" >&2 +echo " cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local" >&2 +echo " cmake --build build && sudo cmake --install build" >&2 +echo " /usr/local/hybbx/hybbx-start" >&2 +exit 1 diff --git a/scripts/mock-ardopc.py b/scripts/mock-ardopc.py new file mode 100755 index 0000000..0a2e110 --- /dev/null +++ b/scripts/mock-ardopc.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Minimal ARDOPC TCP mock for HyBBX plugin host-TCP local tests. +Control port N, data port N+1. CRC-16 poly 0x8810 (ARDOP host family). +""" +from __future__ import annotations + +import socket +import struct +import sys +import threading +import time + +INT_POLY = 0x8810 + + +def crc16(data: bytes) -> int: + reg = 0xFFFF + for byte in data: + mask = 0x80 + for _ in range(8): + bit = byte & mask + mask >>= 1 + if reg & 0x8000: + if bit: + reg = (1 + (reg << 1)) & 0xFFFF + else: + reg = (reg << 1) & 0xFFFF + reg ^= INT_POLY + else: + if bit: + reg = (1 + (reg << 1)) & 0xFFFF + else: + reg = (reg << 1) & 0xFFFF + return reg + + +def frame_cmd(text: str) -> bytes: + body = (text + "\r").encode("ascii") + c = crc16(body) + return body + struct.pack(">H", c) + + +def parse_frames(buf: bytearray) -> list[bytes]: + out: list[bytes] = [] + while True: + if b"\r" not in buf: + break + cr = buf.index(0x0D) + need = cr + 1 + 2 + if len(buf) < need: + break + frame = bytes(buf[:need]) + del buf[:need] + if crc16(frame[:-2]) == struct.unpack(">H", frame[-2:])[0]: + out.append(frame[:-2]) + return out + + +def serve_ctrl(port: int, log: list[str]) -> None: + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(("127.0.0.1", port)) + srv.listen(1) + srv.settimeout(8.0) + try: + conn, _ = srv.accept() + except socket.timeout: + srv.close() + return + conn.settimeout(2.0) + conn.sendall(frame_cmd("VERSION crdopc_1.0.0")) + buf = bytearray() + try: + while True: + try: + chunk = conn.recv(4096) + except TimeoutError: + break + if not chunk: + break + buf.extend(chunk) + for frame in parse_frames(buf): + cmd = frame.decode("ascii", errors="replace").strip() + log.append(f"ctrl<= {cmd}") + if cmd.startswith("INITIALIZE"): + conn.sendall(frame_cmd("RDY")) + elif cmd == "RDY": + pass + else: + conn.sendall(frame_cmd("RDY")) + finally: + conn.close() + srv.close() + + +def serve_data(port: int, log: list[str]) -> None: + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(("127.0.0.1", port)) + srv.listen(1) + srv.settimeout(8.0) + try: + conn, _ = srv.accept() + except socket.timeout: + srv.close() + return + conn.settimeout(1.0) + try: + while True: + try: + chunk = conn.recv(4096) + except socket.timeout: + continue + if not chunk: + break + log.append(f"data<= {len(chunk)} bytes") + finally: + conn.close() + srv.close() + + +def main() -> int: + port = int(sys.argv[1]) if len(sys.argv) > 1 else 18515 + log: list[str] = [] + t1 = threading.Thread(target=serve_ctrl, args=(port, log), daemon=True) + t2 = threading.Thread(target=serve_data, args=(port + 1, log), daemon=True) + t1.start() + t2.start() + time.sleep(6) + for line in log: + print(line) + return 0 if any("INITIALIZE" in x for x in log) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test-ardop-plugin.sh b/scripts/test-ardop-plugin.sh new file mode 100755 index 0000000..67900f1 --- /dev/null +++ b/scripts/test-ardop-plugin.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# Local test: mock ARDOPC + HyBBX ardop plugin (no Main circuit required). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${ROOT}/build" +HYBBX="${BUILD}/src/hybbxd" +MOCK_PORT=18515 +TMPDIR="${ROOT}/local/test-ardop-$$" +mkdir -p "${TMPDIR}" + +cleanup() { + kill "${MOCK_PID:-}" 2>/dev/null || true + kill "${CIRCUIT_PID:-}" 2>/dev/null || true + kill "${HYBBX_PID:-}" 2>/dev/null || true + rm -rf "${TMPDIR}" +} +trap cleanup EXIT + +if [[ ! -x "${HYBBX}" ]]; then + echo "Build hybbx first: cmake --build build" >&2 + exit 1 +fi + +cat > "${TMPDIR}/hybbx.ini" </dev/null 2>&1 & +CIRCUIT_PID=$! +sleep 0.2 + +LOG="${TMPDIR}/hybbx.log" +stdbuf -oL timeout 5 "${HYBBX}" -c "${TMPDIR}/hybbx.ini" >"${LOG}" 2>&1 & +HYBBX_PID=$! +sleep 3 +kill "${HYBBX_PID}" 2>/dev/null || true +wait "${HYBBX_PID}" 2>/dev/null || true +kill "${CIRCUIT_PID}" 2>/dev/null || true + +grep -q "connected to external ARDOPC" "${LOG}" || { + echo "FAIL: no ARDOPC connect log" >&2 + cat "${LOG}" >&2 + exit 1 +} +grep -q "host init sent" "${LOG}" || { + echo "FAIL: no host init log" >&2 + cat "${LOG}" >&2 + exit 1 +} +grep -q "CB half-duplex profile" "${LOG}" || { + echo "FAIL: no CB profile log" >&2 + cat "${LOG}" >&2 + exit 1 +} + +echo "OK: ardop plugin local test passed" +cat "${LOG}" | grep '\[ardop\]' diff --git a/scripts/test-crdop-plugin.sh b/scripts/test-crdop-plugin.sh new file mode 100755 index 0000000..aec10e1 --- /dev/null +++ b/scripts/test-crdop-plugin.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Local test: mock CRDOPC + HyBBX crdop plugin (CB defaults, no Main circuit required). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${ROOT}/build" +HYBBX="${BUILD}/src/hybbxd" +MOCK_PORT=18517 +TMPDIR="${ROOT}/local/test-crdop-$$" +mkdir -p "${TMPDIR}" + +cleanup() { + kill "${MOCK_PID:-}" 2>/dev/null || true + kill "${CIRCUIT_PID:-}" 2>/dev/null || true + kill "${HYBBX_PID:-}" 2>/dev/null || true + rm -rf "${TMPDIR}" +} +trap cleanup EXIT + +if [[ ! -x "${HYBBX}" ]]; then + echo "Build hybbx first: cmake --build build" >&2 + exit 1 +fi + +cat > "${TMPDIR}/hybbx.ini" </dev/null 2>&1 & +CIRCUIT_PID=$! +sleep 0.2 + +LOG="${TMPDIR}/hybbx.log" +stdbuf -oL timeout 5 "${HYBBX}" -c "${TMPDIR}/hybbx.ini" >"${LOG}" 2>&1 & +HYBBX_PID=$! +sleep 3 +kill "${HYBBX_PID}" 2>/dev/null || true +wait "${HYBBX_PID}" 2>/dev/null || true +kill "${CIRCUIT_PID}" 2>/dev/null || true + +grep -q '\[crdop\] connected to external CRDOPC' "${LOG}" || { + echo "FAIL: no CRDOP CRDOPC connect log" >&2 + cat "${LOG}" >&2 + exit 1 +} +grep -q '\[crdop\] modem VERSION' "${LOG}" || { + echo "FAIL: no CRDOP VERSION log" >&2 + cat "${LOG}" >&2 + exit 1 +} +grep -q '\[crdop\] host init sent' "${LOG}" || { + echo "FAIL: no CRDOP host init log" >&2 + cat "${LOG}" >&2 + exit 1 +} +grep -q '\[crdop\] CB half-duplex profile' "${LOG}" || { + echo "FAIL: no CRDOP CB profile log" >&2 + cat "${LOG}" >&2 + exit 1 +} +grep -q 'profile=cb' "${LOG}" || { + echo "FAIL: crdop profile not cb" >&2 + cat "${LOG}" >&2 + exit 1 +} + +echo "OK: crdop plugin local test passed" +grep '\[crdop\]' "${LOG}" diff --git a/share/THIRD_PARTY_NOTICES.txt b/share/THIRD_PARTY_NOTICES.txt new file mode 100644 index 0000000..cf2c7c9 --- /dev/null +++ b/share/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,22 @@ +HyBBX third-party notices +========================= + +libssh +------ + +The SSH transport plugin (`plugins/ssh/`) uses libssh for the SSHv2 server +protocol. + + Project: https://www.libssh.org/ + License: GNU Lesser General Public License v2.1 or later (LGPL-2.1+) + +HyBBX links against the shared libssh library (`libssh.so`) at build and +runtime. HyBBX source code does not incorporate libssh source. + +Under the LGPL you may replace the libssh shared library with a compatible +version without rebuilding HyBBX. Corresponding source code for the libssh +version you use is available from the libssh project and your OS distributor. + + SPDX-License-Identifier: LGPL-2.1-or-later + +See also LICENSE.txt for HyBBX (GPL-3.0). diff --git a/share/areas.yaml b/share/areas.yaml new file mode 100644 index 0000000..8a10d54 --- /dev/null +++ b/share/areas.yaml @@ -0,0 +1,66 @@ +# HyBBX menu layout — areas, per-level menu, and full index. +# Command help, aliases, access, and account rights: commands.yaml (loaded after). + +meta: + menu_header: "HyBBX commands /help for more" + index_header: "HyBBX command-index /help for more" + alias_header: "HyBBX aliases /help for more" + +user_groups: + - Sysop + - Admin + - Mod + - User + - Guest + +areas: + - label: General + commands: [help, menu, index, alias, news, banner, motd, rules, who, users, session, version] + - label: Screen + commands: [clear, echo] + - label: Areas + commands: [leave, main, chat, conference, mail, proxymail, proxychat, exit] + - label: Account + commands: [login, changeme, register, deleteme] + - label: Admin + commands: [usercreate, activate, changeuser, promote, demote, delete] + - label: Sysop + commands: [deleteuser, shutdown, restart, broadcast, monitor] + +menu: + Guest: + - General + - Screen + - Account + - Areas + User: + - General + - Screen + - Areas + - Account + Mod: + - General + - Screen + - Areas + - Account + Admin: + - General + - Screen + - Areas + - Account + - Admin + Sysop: + - General + - Screen + - Areas + - Account + - Admin + - Sysop + +index: + - General + - Screen + - Areas + - Account + - Admin + - Sysop diff --git a/share/cgi/hybbx-telnet.cgi.example b/share/cgi/hybbx-telnet.cgi.example new file mode 100644 index 0000000..ba96ec4 --- /dev/null +++ b/share/cgi/hybbx-telnet.cgi.example @@ -0,0 +1,18 @@ +#!/bin/sh +# Example CGI bridge: hybbx-telnet to local HyBBX (telnet transport). +# Prefer WebSocket + reverse proxy when available — see docs/WEBSOCKET.md. +# +# Install: copy to cgi-bin, chmod +x, set HYBBX_TELNET and paths below. + +HYBBX_TELNET="${HYBBX_TELNET:-/usr/local/hybbx/hybbx-telnet}" +HOST="${HYBBX_HOST:-127.0.0.1}" +PORT="${HYBBX_PORT:-2323}" + +printf 'Content-Type: text/plain; charset=utf-8\r\n\r\n' + +if [ ! -x "$HYBBX_TELNET" ]; then + printf 'hybbx-telnet not found\n' + exit 1 +fi + +exec "$HYBBX_TELNET" -H "$HOST" -p "$PORT" --pace no diff --git a/share/commands.yaml b/share/commands.yaml new file mode 100644 index 0000000..0f97200 --- /dev/null +++ b/share/commands.yaml @@ -0,0 +1,306 @@ +# HyBBX command registry — help text, aliases, access levels, account rights. +# Menu layout and user groups: areas.yaml (loaded first). +# +# Help format (2 lines, max 80 cols): +# / placeholders +# Help: summary. More: subcommands or aliases +# No square brackets in session help/menu text. + +rights: + userchange: + - actor: Sysop + targets: [Admin, Mod, User] + - actor: Admin + targets: [Mod, User] + userdelete: + - actor: Sysop + targets: [Admin, Mod, User, Guest] + delete: + - actor: Sysop + targets: [Admin, Mod, User] + - actor: Admin + targets: [Mod, User] + promote: + - actor: Sysop + to: Admin + from: [User, Mod] + - actor: Sysop + to: Mod + from: [User] + - actor: Admin + to: Mod + from: [User] + demote: + - actor: Sysop + from: [Admin, Mod] + - actor: Admin + from: [Mod] + +aliases: + help: + - "?" + - command + - cmd + - commands + banner: + - loginmsg + rules: + - legal + who: + - online + version: + - ver + session: + - info + leave: + - back + clear: + - cls + - reset + exit: + - logout + - bye + - quit + delete: + - del + conference: + - meeting + usercreate: + - createuser + changeuser: + - userchange + deleteuser: + - userdelete + broadcast: + - announce + monitor: + - mon + proxymail: + - /mail proxymail + proxychat: + - /chat proxychat + +commands: + help: + group: general + min: Guest + line1: "/help cmd" + line2: "Help: Full command-index or /help . More: /menu /alias." + + menu: + group: general + min: Guest + line1: "/menu" + line2: "Help: Full command-index for this Main. Same as /index." + + index: + group: general + min: Guest + line1: "/index" + line2: "Help: Full command-index for this Main. Same as /menu." + + alias: + group: general + min: Guest + line1: "/alias" + line2: "Help: Alternate names for commands. More: see /alias output." + + news: + group: general + min: Guest + line1: "/news" + line2: "Help: Show system news from news.txt." + + banner: + group: general + min: Guest + line1: "/banner" + line2: "Help: Show login banner from banner.txt. More: /loginmsg" + + motd: + group: general + min: Guest + line1: "/motd" + line2: "Help: Show message of the day." + + rules: + group: general + min: Guest + line1: "/rules" + line2: "Help: Show terms of use from rules.txt. More: /legal" + + who: + group: general + min: Guest + line1: "/who" + line2: "Help: Online interactive users (telnet/SSH/Web). More: /online" + + users: + group: general + min: User + line1: "/users" + line2: "Help: Registered users by level (Sysop Admin Mod User Guest)." + + session: + group: general + min: Guest + line1: "/session" + line2: "Help: Username, session id, transport. More: /info" + + version: + group: general + min: Guest + line1: "/version" + line2: "Help: Show text/version.txt (@version@ @os@). More: /ver" + + clear: + group: screen + min: Guest + line1: "/clear" + line2: "Help: Clear screen and discard input line. More: /cls /reset" + + echo: + group: screen + min: Guest + line1: "/echo yes|no" + line2: "Help: Show or set typed-character echo." + + leave: + group: areas + min: User + line1: "/leave" + line2: "Help: Up one area level. More: /back" + + main: + group: areas + min: User + line1: "/main" + line2: "Help: Return to main prompt." + + chat: + group: areas + min: User + line1: "/chat n|name|show|showall|proxychat" + line2: "Help: Join or list chat channels. More: /chat proxychat" + + conference: + group: areas + min: User + line1: "/conference topic user" + line2: "Help: Private two-user channel. More: /meeting" + + mail: + group: areas + min: User + line1: "/mail list|read|send|delete|recycle|proxymail ..." + line2: "Help: Personal mailbox on this Main. More: list read send delete" + + proxymail: + group: areas + min: User + line1: "/proxymail" + line2: "Help: mail to user@other-main. More: list read send delete recycle." + + proxychat: + group: areas + min: User + line1: "/proxychat" + line2: "Help: Chat with users on other mains. More: /chat proxychat" + + exit: + group: areas + min: Guest + line1: "/exit" + line2: "Help: Close connection. More: /quit /logout /bye" + + login: + group: account + min: Guest + line1: "/login username password" + line2: "Help: Log in with a registered account." + + register: + group: account + min: Guest + only: Guest + line1: "/register user name country location email password" + line2: "Help: Self-registration. Guest only. Sysop and Admin notified." + + changeme: + group: account + min: User + line1: "/changeme old new name country location email" + line2: "Help: Update own profile and password." + + deleteme: + group: account + min: User + max: Admin + line1: "/deleteme yes|no" + line2: "Help: Permanently delete own account." + + usercreate: + group: admin + min: Admin + line1: "/usercreate user name country location email" + line2: "Help: Create user account. Inactive until /activate. More: /createuser" + + activate: + group: admin + min: Admin + line1: "/activate username" + line2: "Help: Set username activated." + + changeuser: + group: admin + min: Admin + line1: "/changeuser user name country location email password" + line2: "Help: Overwrite user profile and password. More: /userchange" + + promote: + group: admin + min: Admin + line1: "/promote username admin|mod" + line2: "Help: Raise user to Admin or Mod." + + demote: + group: admin + min: Admin + line1: "/demote username" + line2: "Help: Set user to User level." + + delete: + group: admin + min: Admin + line1: "/delete username" + line2: "Help: Permanently remove account. More: /del" + + deleteuser: + group: sysop + min: Sysop + line1: "/deleteuser username" + line2: "Help: Delete any non-Sysop account. More: /userdelete" + + shutdown: + group: sysop + min: Sysop + line1: "/shutdown" + line2: "Help: Stop the HyBBX daemon." + + restart: + group: sysop + min: Sysop + line1: "/restart" + line2: "Help: Restart the HyBBX daemon." + + broadcast: + group: sysop + min: Sysop + line1: "/broadcast message | ax25" + line2: "Help: Local +++ announce or sequential RF beacon. More: /announce" + + monitor: + group: sysop + min: Sysop + line1: "/monitor on|off|list|cmds|meet user" + line2: "Help: Live log monitor and sysop admin menu. More: /mon" diff --git a/share/data.gitkeep b/share/data.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/share/fail2ban/filter.d/hybbx-circuit.conf b/share/fail2ban/filter.d/hybbx-circuit.conf new file mode 100644 index 0000000..901e8a2 --- /dev/null +++ b/share/fail2ban/filter.d/hybbx-circuit.conf @@ -0,0 +1,10 @@ +# HyBBX HBX circuit link authentication failures (security.log). +# Matches failed LINK_AUTH on [circuit] port (default 7323). + +[INCLUDES] +before = common.conf + +[Definition] +_daemon = hybbx +failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} link_auth_fail ip= .*transport=circuit +ignoreregex = diff --git a/share/fail2ban/filter.d/hybbx-ssh.conf b/share/fail2ban/filter.d/hybbx-ssh.conf new file mode 100644 index 0000000..407be52 --- /dev/null +++ b/share/fail2ban/filter.d/hybbx-ssh.conf @@ -0,0 +1,11 @@ +# HyBBX SSH transport login brute-force (security.log). +# Enable when [transport.ssh] is running and logs login_fail +# with transport=ssh to security.log. + +[INCLUDES] +before = common.conf + +[Definition] +_daemon = hybbx +failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} login_fail ip= user=.* transport=ssh +ignoreregex = diff --git a/share/fail2ban/filter.d/hybbx-telnet.conf b/share/fail2ban/filter.d/hybbx-telnet.conf new file mode 100644 index 0000000..f6dfa53 --- /dev/null +++ b/share/fail2ban/filter.d/hybbx-telnet.conf @@ -0,0 +1,10 @@ +# HyBBX telnet login brute-force (security.log). +# Install: copy to /etc/fail2ban/filter.d/ and enable hybbx-telnet in jail.d. + +[INCLUDES] +before = common.conf + +[Definition] +_daemon = hybbx +failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} login_fail ip= user=.* transport=telnet +ignoreregex = diff --git a/share/fail2ban/filter.d/hybbx-websocket.conf b/share/fail2ban/filter.d/hybbx-websocket.conf new file mode 100644 index 0000000..2166ee7 --- /dev/null +++ b/share/fail2ban/filter.d/hybbx-websocket.conf @@ -0,0 +1,11 @@ +# HyBBX WebSocket transport login brute-force (security.log). +# Enable when [transport.websocket] is running and logs login_fail +# with transport=websocket to security.log. + +[INCLUDES] +before = common.conf + +[Definition] +_daemon = hybbx +failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} login_fail ip= user=.* transport=websocket +ignoreregex = diff --git a/share/fail2ban/jail.d/hybbx.local.example b/share/fail2ban/jail.d/hybbx.local.example new file mode 100644 index 0000000..3d91296 --- /dev/null +++ b/share/fail2ban/jail.d/hybbx.local.example @@ -0,0 +1,45 @@ +# Example fail2ban jails for HyBBX security.log +# +# HyBBX v2.0.0+ includes built-in [security] bans — these filters are optional +# for site-wide iptables/nftables integration alongside the daemon. +# +# 1. Copy filter.d/*.conf to /etc/fail2ban/filter.d/ +# 2. Copy this file to /etc/fail2ban/jail.d/hybbx.local +# 3. Set logpath to your HyBBX logs directory ([log] dir in hybbx.ini) +# 4. sudo fail2ban-client reload + +[hybbx-telnet] +enabled = true +filter = hybbx-telnet +port = 2323 +logpath = /usr/local/hybbx/logs/security.log +maxretry = 5 +findtime = 600 +bantime = 600 + +[hybbx-circuit] +enabled = true +filter = hybbx-circuit +port = 7323 +logpath = /usr/local/hybbx/logs/security.log +maxretry = 5 +findtime = 600 +bantime = 600 + +[hybbx-ssh] +enabled = false +filter = hybbx-ssh +port = 3232 +logpath = /usr/local/hybbx/logs/security.log +maxretry = 5 +findtime = 600 +bantime = 600 + +[hybbx-websocket] +enabled = false +filter = hybbx-websocket +port = 4591 +logpath = /usr/local/hybbx/logs/security.log +maxretry = 5 +findtime = 600 +bantime = 600 diff --git a/share/hybbx-main.ini.example b/share/hybbx-main.ini.example new file mode 100644 index 0000000..8573620 --- /dev/null +++ b/share/hybbx-main.ini.example @@ -0,0 +1,97 @@ +; SUPERSEDED — use share/hybbxd-main.ini.example (hybbxd Main instance) +; HyBBX v2.8.0 — Main (users + HBX hub; RF on remote Secondary) +; Pair with: share/hybbx-secondary.ini.example on the remote RF host +; Copy: cp share/hybbx-main.ini.example hybbx.ini + +[service] +name = hyBBX +login_announce = no +; login_announce = yes — "*** User login: user@plugin" (not Guest; not invisible Sysop); +; /who and /online list as user@plugin + +[storage] +backend = flatfile +path = data + +[auth] +auto_login = yes + +[traffic] +baud = 2400 +line_width = 80 +pace_output = yes +ansi = no + +[log] +enabled = yes +dir = logs +level = warn + + +[monitor] +enabled = yes +follow_hybbx = yes +follow_security = yes +invisible-sysop = no +invite_timeout_sec = 20 +; allow = Alice,Bob +; invisible-sysop = yes — Sysop always hidden from /who (even without /monitor on) + +[security] +enabled = yes +telnet = yes +ssh = yes +websocket = yes +circuit = yes + +[mail] +enabled = yes +path = mail + +[networks] +ax25 = no +baycom = no +tmodem = no +ssh = yes +websocket = yes +circuit = yes +mains_proxy = no + +[transport.telnet] +bind = 0.0.0.0 +port = 2323 + +[transport.ssh] +enabled = yes +bind = 0.0.0.0 +port = 3232 +hostkey_dir = keys + +[circuit] +bind = 127.0.0.1 +port = 7323 +link_auth = yes +link_password = changeme +max_links = 8 +balance = yes + +[broadcast] +enabled = yes +ax25 = yes +ax25_auto = yes +ax25_auto_interval = 300 +ax25_auto_stagger = 0 +ax25_auto_message = Broadcast: @service@ online +ax25_mycall = HYBBX +ax25_dest = QST + +[ax25] +frequency1 = 27.205 +frequency1_label = remote-A + +; Bridge registry — link_id/password/MHz must match Secondary [transport.packet_radioN] +[transport.packet_radio1] +link_id = secondary-1 +link_password = changeme +link_role = link +frequency_mhz = 27.205 diff --git a/share/hybbx-mesh.ini.example b/share/hybbx-mesh.ini.example new file mode 100644 index 0000000..6225048 --- /dev/null +++ b/share/hybbx-mesh.ini.example @@ -0,0 +1,93 @@ +; SUPERSEDED — use share/hybbxd-main.ini.example + hybbxsd-secondary / hybbxpd-proxy +; HyBBX v2.8.0 — Main + proxy mesh (mains_proxy) + remote Secondary RF +; Build: -DHYBBX_PLUGIN_MAINS_PROXY=ON · Pair Secondary: hybbx-secondary.ini.example +; Reciprocal peer INI required on remote Main. Docs: docs/MAINS_PROXY.md + +[service] +name = main-west +login_announce = no +; login_announce = yes — "*** User login: user@plugin" (not Guest; not invisible Sysop); +; /who and /online list as user@plugin + +[storage] +backend = flatfile +path = data + +[auth] +auto_login = yes + +[log] +enabled = yes +dir = logs +level = warn + + +[monitor] +enabled = yes +follow_hybbx = yes +follow_security = yes +invisible-sysop = no +invite_timeout_sec = 20 +; allow = Alice,Bob +; invisible-sysop = yes — Sysop always hidden from /who (even without /monitor on) + +[security] +enabled = yes +telnet = yes +ssh = yes +circuit = yes + +[mail] +enabled = yes +path = mail + +[networks] +ax25 = no +baycom = no +tmodem = no +ssh = yes +circuit = yes +mains_proxy = yes + +[transport.telnet] +bind = 0.0.0.0 +port = 2323 + +[circuit] +bind = 127.0.0.1 +port = 7323 +link_auth = yes +link_password = changeme +max_links = 8 +balance = yes + +[broadcast] +enabled = yes +ax25 = yes +ax25_auto = yes +ax25_auto_interval = 300 + +[ax25] +frequency1 = 27.205 + +; Outbound proxy peer (HBX to remote Main) +[transport.mains_proxy1] +peer_id = main-east +circuit_host = 192.0.2.10 +circuit_port = 7323 +link_id = main-west +link_password = changeme +wire = circuit +duplex = full + +; Inbound registry for remote peer (match their link_id on LINK_AUTH) +[transport.mains_proxy2] +peer_id = main-east +link_id = main-east +link_password = changeme + +; RF bridge — remote Secondary packet_radio +[transport.packet_radio1] +link_id = secondary-1 +link_password = changeme +frequency_mhz = 27.205 diff --git a/share/hybbx-secondary.ini.example b/share/hybbx-secondary.ini.example new file mode 100644 index 0000000..ab9db5e --- /dev/null +++ b/share/hybbx-secondary.ini.example @@ -0,0 +1,64 @@ +; SUPERSEDED — use share/hybbxsd-secondary.ini.example (hybbxsd Secondary instance) +; HyBBX v2.8.0 — Secondary (RF host → Main HBX hub) +; Pair with: share/hybbx-main.ini.example or share/hybbx-mesh.ini.example +; Prerequisite: max25d preps serial on this host — HyBBX KISS attach only. +; link_id / link_password / frequency_mhz must match Main [transport.packet_radioN] + +[service] +name = hyBBX-secondary +login_announce = no +; login_announce = yes — "*** User login: user@plugin" (not Guest; not invisible Sysop); +; /who and /online list as user@plugin + +[storage] +backend = flatfile +path = data-secondary + +[auth] +auto_login = no + +[log] +enabled = yes +dir = logs +level = warn + + +[monitor] +enabled = yes +follow_hybbx = yes +follow_security = yes +invisible-sysop = no +invite_timeout_sec = 20 +; allow = Alice,Bob +; invisible-sysop = yes — Sysop always hidden from /who (even without /monitor on) + +[networks] +ax25 = yes +baycom = no +tmodem = no +circuit = no + +[max25] +check = yes +host = 127.0.0.1 +port = 7325 +; error/voice STATUS: max25d [reporting] only (3×20s passes, 50% min good, defaults) + +[transport.packet_radio1] +circuit_host = main.example.com +circuit_port = 7323 +link_id = secondary-1 +link_password = changeme +frequency_mhz = 27.205 +tnc = tnc2c +protocol = kiss +device = /dev/ttyUSB0 +baud = 9600 +kiss_entry = none +modem = tcm3105 +radio_baud = 1200 +radio_band = amateur +radio_duplex = half + +[ax25] +frequency1 = 27.205 diff --git a/share/hybbx-standalone.ini.example b/share/hybbx-standalone.ini.example new file mode 100644 index 0000000..86cff32 --- /dev/null +++ b/share/hybbx-standalone.ini.example @@ -0,0 +1,125 @@ +; HyBBX v2.8.0 — Standalone Main (users + local RF on one host) +; Copy: cp share/hybbx-standalone.ini.example hybbx.ini +; Prerequisite: max25d (MAX25-Stack) preps serial/KISS — HyBBX attach only. +; PC-COM / BayCom/based: max25d max25e0 → /tmp/max25-bcpr/kiss-bc0 +; Docs: docs/MANUAL.md · docs/TNCS.md · docs/BAYCOM.md · docs/TOPOLOGY.md + +[instance] +standalone = yes + +[service] +name = hyBBX +; Process owner — required when started as root (hybbxd must not stay root) +user = hybbx +group = hybbx +login_announce = no +; login_announce = yes — "*** User login: user@plugin" (not Guest; not invisible Sysop); +; /who and /online list as user@plugin + +[storage] +backend = flatfile +path = data + +[auth] +auto_login = yes + +[traffic] +baud = 2400 +line_width = 80 +pace_output = yes +ansi = no + +[log] +enabled = yes +dir = logs +level = warn + + +[monitor] +enabled = yes +follow_hybbx = yes +follow_security = yes +invisible-sysop = no +invite_timeout_sec = 20 +; allow = Alice,Bob +; invisible-sysop = yes — Sysop always hidden from /who (even without /monitor on) + +[security] +enabled = yes +telnet = yes +ssh = yes +circuit = yes + +[mail] +enabled = yes +path = mail + +[networks] +; Standalone Main: enable local RF transports (TNC and/or BayCom/based) +ax25 = no +baycom = yes +tmodem = no +ssh = yes +circuit = yes + +[max25] +check = yes +host = 127.0.0.1 +port = 7325 +; error/voice STATUS: max25d [reporting] only (3×20s passes, 50% min good, defaults) + +[circuit] +bind = 127.0.0.1 +port = 7323 +link_auth = yes +link_password = changeme +max_links = 8 +balance = yes + +[broadcast] +enabled = yes +ax25 = yes +ax25_auto = yes +ax25_auto_interval = 300 +ax25_auto_stagger = 0 +ax25_auto_message = Broadcast: @service@ online +ax25_mycall = HYBBX +ax25_dest = QST + +[ax25] +frequency1 = 27.205 +frequency1_label = local-A + +; BayCom/based via MAX25 prep — KISS attach (PC-COM / SER12 userspace path) +[transport.baycom1] +backend = kiss +device = /tmp/max25-bcpr/kiss-bc0 +serial_baud = 1200 +radio_baud = 1200 +txdelay = 35 +slot = 10 +persist = 128 +txtail = 2 +circuit_host = 127.0.0.1 +circuit_port = 7323 +link_id = local-pccom +link_password = changeme +link_role = link +frequency_mhz = 27.205 + +; Optional TNC path (enable [networks] ax25=yes and comment out baycom if TNC-only) +; [transport.packet_radio1] +; link_id = local-radio1 +; link_password = changeme +; frequency_mhz = 27.205 +; circuit_host = 127.0.0.1 +; circuit_port = 7323 +; tnc = tnc2c +; protocol = kiss +; device = /dev/ttyUSB0 +; baud = 9600 +; kiss_entry = none +; modem = tcm3105 +; radio_baud = 1200 +; radio_band = amateur +; radio_duplex = half diff --git a/share/hybbx.ini.example b/share/hybbx.ini.example new file mode 100644 index 0000000..f730f46 --- /dev/null +++ b/share/hybbx.ini.example @@ -0,0 +1,17 @@ +; HyBBX v2.8.0 — INI topology (three instance binaries) +; +; Layout (hardcoded per binary): +; +; hybbxd Main — local BBX; WebSocket→reverse-proxy; mains_proxy↔Secondary +; hybbxsd Secondary — peer Mains via mains_proxy; transport plugins; no user BBX +; hybbxpd Proxy — TCP hybrid bridge; circuit→Main; no WebSocket/RF in-process +; +; Templates: +; hybbxd-main.ini.example Main (hybbxd) +; hybbxsd-secondary.ini.example Secondary (hybbxsd) +; hybbxpd-proxy.ini.example Proxy (hybbxpd) +; +; Legacy (superseded): hybbx-main.ini.example, hybbx-secondary.ini.example, +; hybbx-mesh.ini.example, hybbx-standalone.ini.example +; +; Default install: hybbxd-main.ini.example → hybbx.ini diff --git a/share/hybbxd-main.ini.example b/share/hybbxd-main.ini.example new file mode 100644 index 0000000..efcadf6 --- /dev/null +++ b/share/hybbxd-main.ini.example @@ -0,0 +1,82 @@ +; HyBBX Main — binary: hybbxd +; Local BBX (users/mail/chat). Remote users: WebSocket → reverse-proxy only. +; HBX circuit hub for Proxy (hybbxpd). mains_proxy for Secondary peers (hybbxsd). +; Copy: cp share/hybbxd-main.ini.example local/hybbx.ini + +[service] +name = hyBBX +; Required when hybbxd is started as root — process drops here before plugins bind +user = hybbx +group = hybbx +login_announce = no + +[storage] +backend = flatfile +path = data + +[auth] +auto_login = yes + +[traffic] +baud = 2400 +line_width = 80 +pace_output = yes +ansi = no + +[log] +enabled = yes +dir = logs +level = warn + +[monitor] +enabled = yes +follow_hybbx = yes +follow_security = yes +invisible-sysop = no +invite_timeout_sec = 20 + +[security] +enabled = yes +telnet = no +ssh = no +websocket = yes +circuit = yes + +[mail] +enabled = yes +path = mail + +[networks] +; Main layout: WebSocket user path + circuit hub + optional mains_proxy +telnet = no +ssh = no +websocket = yes +ax25 = no +baycom = no +tmodem = no +ardop = no +crdop = no +circuit = yes +mains_proxy = no + +[transport.websocket] +bind = 127.0.0.1 +port = 8080 +; Put nginx/caddy reverse-proxy in front for public TLS. + +[circuit] +enabled = yes +bind = 0.0.0.0 +port = 7323 +link_auth = yes +link_password = changeme +max_links = 8 +balance = yes + +; Example Proxy registry row (hybbxpd attaches with matching link_id/password) +; [transport.packet_radio1] — superseded: RF via MAX25 + hybbxpd, not Main packet_radio +[transport.baycom1] +link_id = proxy-1 +link_password = changeme +link_role = link +; Bridge credentials only on Main — no local device on hybbxd diff --git a/share/hybbxpd-proxy.ini.example b/share/hybbxpd-proxy.ini.example new file mode 100644 index 0000000..26a4f4b --- /dev/null +++ b/share/hybbxpd-proxy.ini.example @@ -0,0 +1,61 @@ +; HyBBX Proxy — binary: hybbxpd +; Hybrid bridge (router/gateway/relay posture). TCP/IP primary. +; No WebSocket. No in-process AX.25/packet_radio — RF via MAX25-Stack +; (relay / gateway / digipeater for hyBBX). +; Attach to Main (hybbxd) over HBX circuit. +; Copy: cp share/hybbxpd-proxy.ini.example local/hybbxpd.ini + +[service] +name = hyBBX-proxy + +[storage] +backend = flatfile +path = data-proxy + +[auth] +auto_login = no + +[log] +enabled = yes +dir = logs +level = warn + +[networks] +telnet = yes +ssh = yes +websocket = no +ax25 = no +baycom = no +tmodem = no +ardop = no +crdop = no +circuit = yes +mains_proxy = no + +[transport.telnet] +bind = 127.0.0.1 +port = 2323 + +[transport.ssh] +bind = 0.0.0.0 +port = 3232 + +; Client attach toward Main hub (not a circuit listener) +[circuit] +enabled = yes +; Proxy uses circuit_host on edge transports — hub stays on Main. + +; Example: TCP/hybrid edge toward Main (credentials must match Main bridge row) +[transport.baycom1] +enabled = no +; Prefer MAX25 relay/gateway/digipeater instead of in-process BayCom RF. +circuit_host = 192.0.2.1 +circuit_port = 7323 +link_id = proxy-1 +link_password = changeme +link_role = link + +[max25] +check = yes +host = 127.0.0.1 +port = 7325 diff --git a/share/hybbxsd-secondary.ini.example b/share/hybbxsd-secondary.ini.example new file mode 100644 index 0000000..25d737e --- /dev/null +++ b/share/hybbxsd-secondary.ini.example @@ -0,0 +1,41 @@ +; HyBBX Secondary — binary: hybbxsd +; Peer other Mains via mains_proxy only. Not a local user BBX (no hybbxd behaviour). +; Copy: cp share/hybbxsd-secondary.ini.example local/hybbxsd.ini + +[service] +name = hyBBX-secondary-peer + +[storage] +backend = flatfile +path = data-secondary + +[auth] +auto_login = no + +[log] +enabled = yes +dir = logs +level = warn + +[networks] +; Secondary = other-Main peers (mains_proxy). No user telnet/ssh/websocket. +telnet = no +ssh = no +websocket = no +ax25 = no +baycom = no +tmodem = no +ardop = no +crdop = no +circuit = no +mains_proxy = yes + +; Reciprocal peer on remote Main (hybbxd with mains_proxy=yes) +[transport.mains_proxy1] +enabled = yes +peer_id = main-east +circuit_host = 192.0.2.10 +circuit_port = 7323 +link_id = mesh-west-east +link_password = changeme +link_role = proxy diff --git a/share/logs.gitkeep b/share/logs.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/share/reverse-proxy/README.txt b/share/reverse-proxy/README.txt new file mode 100644 index 0000000..a11d1cc --- /dev/null +++ b/share/reverse-proxy/README.txt @@ -0,0 +1,6 @@ +HyBBX reverse-proxy examples. WebSocket plugin = session data on loopback only. + +1. HTTPD_DOCROOT=/srv/www # your httpd document root +2. cp -r docroot/hybbx-websocket $HTTPD_DOCROOT/ +3. Add nginx/apache/lighttpd snippet (adjust paths) +4. hybbx.ini [transport.websocket] — see docs/WEBSOCKET.md diff --git a/share/reverse-proxy/apache2.conf.example b/share/reverse-proxy/apache2.conf.example new file mode 100644 index 0000000..a361532 --- /dev/null +++ b/share/reverse-proxy/apache2.conf.example @@ -0,0 +1,24 @@ +# HyBBX — Apache httpd 2.4. HTTPD_DOCROOT example: /srv/www + + + ProxyPreserveHost On + ProxyTimeout 3600 + SSLProxyEngine On + SSLProxyVerify none + SSLProxyCheckPeerCN off + SSLProxyCheckPeerName off + + ProxyPass "/hybbx-websocket/ws" "wss://127.0.0.1:4591/hybbx" timeout=3600 keepalive=On + ProxyPassReverse "/hybbx-websocket/ws" "wss://127.0.0.1:4591/hybbx" + + +Alias /hybbx-websocket /srv/www/hybbx-websocket + + DirectoryIndex index.php + Require all granted + + SetHandler application/x-httpd-php + + + +# UI: cp -r reverse-proxy/docroot/hybbx-websocket $HTTPD_DOCROOT/ diff --git a/share/reverse-proxy/docroot/README.txt b/share/reverse-proxy/docroot/README.txt new file mode 100644 index 0000000..0c820fe --- /dev/null +++ b/share/reverse-proxy/docroot/README.txt @@ -0,0 +1,7 @@ +Copy docroot/hybbx-websocket/ to HTTPD_DOCROOT (not HYBBX_ROOT). +HyBBX websocket plugin ships session data only on loopback :4591. + + HTTPD_DOCROOT=/srv/www + cp -r docroot/hybbx-websocket $HTTPD_DOCROOT/ + +Match URL paths in hybbx.ini and the httpd snippets in docs/WEBSOCKET.md. diff --git a/share/reverse-proxy/docroot/hybbx-websocket/hybbx-terminal.js b/share/reverse-proxy/docroot/hybbx-websocket/hybbx-terminal.js new file mode 100644 index 0000000..50fb826 --- /dev/null +++ b/share/reverse-proxy/docroot/hybbx-websocket/hybbx-terminal.js @@ -0,0 +1,95 @@ +(function () { + 'use strict'; + + var term = document.getElementById('term'); + var status = document.getElementById('status'); + var form = document.getElementById('input-bar'); + var input = document.getElementById('cmd'); + var wsUrl = window.HYBBX_WS_URL; + var ws = null; + var reconnectTimer = null; + var reconnectDelayMs = 2000; + var reconnectAttempt = 0; + + function append(text) { + term.textContent += text; + term.scrollTop = term.scrollHeight; + } + + function setStatus(s) { + status.textContent = s; + } + + function scheduleReconnect() { + if (reconnectTimer) { + return; + } + + reconnectAttempt += 1; + setStatus('reconnecting in ' + (reconnectDelayMs / 1000) + 's'); + reconnectTimer = window.setTimeout(function () { + reconnectTimer = null; + connect(); + }, reconnectDelayMs); + } + + function setInputEnabled(enabled) { + input.disabled = !enabled; + if (enabled) { + input.focus(); + } + } + + function sendLine(text) { + if (!ws || ws.readyState !== WebSocket.OPEN) { + return; + } + + ws.send(text + '\r'); + } + + function connect() { + setStatus('connecting'); + setInputEnabled(false); + ws = new WebSocket(wsUrl); + + ws.onopen = function () { + reconnectAttempt = 0; + setStatus('connected'); + setInputEnabled(true); + }; + + ws.onmessage = function (ev) { + append(ev.data); + }; + + ws.onclose = function (ev) { + var reason = ev.reason ? ' (' + ev.reason + ')' : ''; + + append('\n[websocket disconnected: code ' + ev.code + reason + ']\n'); + setStatus('disconnected: ' + ev.code); + ws = null; + setInputEnabled(false); + scheduleReconnect(); + }; + + ws.onerror = function () { + setStatus('error'); + setInputEnabled(false); + }; + } + + form.addEventListener('submit', function (ev) { + ev.preventDefault(); + + if (!ws || ws.readyState !== WebSocket.OPEN) { + return; + } + + sendLine(input.value); + input.value = ''; + input.focus(); + }); + + connect(); +})(); diff --git a/share/reverse-proxy/docroot/hybbx-websocket/index.php b/share/reverse-proxy/docroot/hybbx-websocket/index.php new file mode 100644 index 0000000..52a060c --- /dev/null +++ b/share/reverse-proxy/docroot/hybbx-websocket/index.php @@ -0,0 +1,89 @@ + + + + + + HyBBX + + + +
disconnected
+

+  
+ +
+ + + + diff --git a/share/reverse-proxy/lighttpd.conf.example b/share/reverse-proxy/lighttpd.conf.example new file mode 100644 index 0000000..1ef9244 --- /dev/null +++ b/share/reverse-proxy/lighttpd.conf.example @@ -0,0 +1,26 @@ +# HyBBX — lighttpd 1.4. HTTPD_DOCROOT example: /srv/www + +# Raise idle limits for upgraded WebSocket connections. +server.max-read-idle = 3600 +server.max-write-idle = 3600 + +$HTTP["url"] == "/hybbx-websocket/ws" { + proxy.server = ( "" => ( + "hybbx" => ( + "proxy" => "https://127.0.0.1:4591/hybbx", + "upgrade" => "enable" + ) + )) + proxy.header = ( "upgrade" => "enable" ) +} + +alias.url += ( "/hybbx-websocket/" => "/srv/www/hybbx-websocket/" ) +index-file.names += ( "index.php" ) + +fastcgi.server += ( ".php" => + ( "localhost" => + ( "socket" => "/run/php-fpm/www.sock" ) + ) +) + +# UI: cp -r reverse-proxy/docroot/hybbx-websocket $HTTPD_DOCROOT/ diff --git a/share/reverse-proxy/nginx.conf.example b/share/reverse-proxy/nginx.conf.example new file mode 100644 index 0000000..c916379 --- /dev/null +++ b/share/reverse-proxy/nginx.conf.example @@ -0,0 +1,28 @@ +# HyBBX — nginx. Set HTTPD_DOCROOT (example: /srv/www). + +location = /hybbx-websocket/ws { + proxy_pass https://127.0.0.1:4591/hybbx; + proxy_ssl_verify off; + proxy_http_version 1.1; + proxy_socket_keepalive on; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location /hybbx-websocket/ { + root /srv/www; + index index.php; + location ~ \.php$ { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_pass unix:/run/php-fpm/www.sock; + } +} + +# UI: cp -r reverse-proxy/docroot/hybbx-websocket $HTTPD_DOCROOT/ +# Plain ws: proxy_pass http://127.0.0.1:4591/hybbx; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..e26bbcd --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,204 @@ +if(HYBBX_BUILD_DAEMON) + add_library(hybbx_core STATIC + core/registry.c + core/service.c + core/config.c + core/instance.c + core/circuit.c + core/circuit_tcp.c + core/circuit_bridge.c + core/circuit_balance.c + core/bandwidth_policy.c + core/broadcast.c + core/link.c + core/command.c + core/command_parse.c + core/commands_registry.c + core/auth.c + core/password.c + core/crypto_config.c + core/crypto_backends.c + core/crypto.c + core/traffic.c + core/storage.c + core/storage_flatfile.c + core/storage_sql.c + core/mail_sql.c + core/proxymail.c + core/proxychat.c + core/session.c + core/chat.c + core/conference.c + core/mail.c + core/messages.c + core/mains_proxy.c + core/networks.c + core/instance.c + core/crdop.c + core/terminal.c + core/texts.c + core/util.c + core/log.c + core/monitor.c + core/max25.c + core/security.c + core/security_ban.c + core/daemon_wrap.c + core/privilege.c + core/rf_tx_pace.c + clients/circuit_client.c + clients/link_auth.c + ${CMAKE_SOURCE_DIR}/third_party/tinysha256/tinysha256.c + ${CMAKE_SOURCE_DIR}/third_party/monocypher/monocypher.c + ${CMAKE_SOURCE_DIR}/third_party/tinyaes/aes.c + ${CMAKE_SOURCE_DIR}/third_party/tinyaes/aes256gcm.c + ) + + if(HYBBX_HAVE_OPENSSL) + target_sources(hybbx_core PRIVATE core/crypto_openssl.c) + endif() + + if(HYBBX_HAVE_LIBSODIUM) + target_sources(hybbx_core PRIVATE core/crypto_libsodium.c) + endif() + + add_library(HyBBX::core ALIAS hybbx_core) + + target_include_directories(hybbx_core + PUBLIC + $ + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core + ${CMAKE_SOURCE_DIR}/third_party/tinysha256 + ${CMAKE_SOURCE_DIR}/third_party/monocypher + ${CMAKE_SOURCE_DIR}/third_party/tinyaes + ) + + target_compile_definitions(hybbx_core + PRIVATE + HYBBX_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + HYBBX_VERSION_MINOR=${PROJECT_VERSION_MINOR} + HYBBX_VERSION_PATCH=${PROJECT_VERSION_PATCH} + ) + + hybbx_apply_hardening(hybbx_core) + + find_package(Threads REQUIRED) + target_link_libraries(hybbx_core PUBLIC Threads::Threads) + + if(HYBBX_HAVE_OPENSSL) + target_compile_definitions(hybbx_core PRIVATE HYBBX_HAVE_OPENSSL) + target_link_libraries(hybbx_core PRIVATE OpenSSL::Crypto) + endif() + + if(HYBBX_HAVE_LIBSODIUM) + target_compile_definitions(hybbx_core PRIVATE HYBBX_HAVE_LIBSODIUM) + target_include_directories(hybbx_core PRIVATE ${LIBSODIUM_INCLUDE_DIRS}) + target_link_directories(hybbx_core PRIVATE ${LIBSODIUM_LIBRARY_DIRS}) + target_link_libraries(hybbx_core PRIVATE ${LIBSODIUM_LIBRARIES}) + endif() + + if(HYBBX_HAVE_SQLITE) + target_compile_definitions(hybbx_core PRIVATE HYBBX_HAVE_SQLITE) + target_include_directories(hybbx_core PRIVATE ${SQLITE3_INCLUDE_DIRS}) + target_link_directories(hybbx_core PRIVATE ${SQLITE3_LIBRARY_DIRS}) + target_link_libraries(hybbx_core PRIVATE ${SQLITE3_LIBRARIES}) + endif() + + add_executable(hybbxd main.c instance_role_main.c) + add_executable(hybbxsd main.c instance_role_secondary.c) + add_executable(hybbxpd main.c instance_role_proxy.c) + + foreach(_bin IN ITEMS hybbxd hybbxsd hybbxpd) + target_link_libraries(${_bin} PRIVATE hybbx_core) + hybbx_apply_hardening_executable(${_bin}) + endforeach() + + install(TARGETS hybbxd hybbxsd hybbxpd hybbx_core + EXPORT HyBBXTargets + RUNTIME DESTINATION ${HYBBX_INSTALL_HOME} + ) + + if(HYBBX_INSTALL_DEV) + install(TARGETS hybbx_core + EXPORT HyBBXTargets + LIBRARY DESTINATION ${HYBBX_INSTALL_HOME}/lib + ARCHIVE DESTINATION ${HYBBX_INSTALL_HOME}/lib + ) + + install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/hybbx + DESTINATION ${HYBBX_INSTALL_HOME}/include + FILES_MATCHING PATTERN "*.h" + ) + + install(EXPORT HyBBXTargets + FILE HyBBXTargets.cmake + NAMESPACE HyBBX:: + DESTINATION ${HYBBX_INSTALL_HOME}/lib/cmake/HyBBX + ) + endif() + + install(FILES + ${CMAKE_SOURCE_DIR}/share/hybbx.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbxd-main.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbxsd-secondary.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbxpd-proxy.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbx-standalone.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbx-main.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbx-mesh.ini.example + ${CMAKE_SOURCE_DIR}/share/hybbx-secondary.ini.example + DESTINATION ${HYBBX_INSTALL_HOME} + ) + + install(FILES ${CMAKE_SOURCE_DIR}/share/hybbxd-main.ini.example + DESTINATION ${HYBBX_INSTALL_HOME} + RENAME hybbx.ini + ) + + install(FILES ${CMAKE_SOURCE_DIR}/share/commands.yaml + ${CMAKE_SOURCE_DIR}/share/areas.yaml + DESTINATION ${HYBBX_INSTALL_HOME} + ) + + install(DIRECTORY ${CMAKE_SOURCE_DIR}/text/ + DESTINATION ${HYBBX_INSTALL_HOME}/text + FILES_MATCHING PATTERN "*.txt" + ) + + install(FILES ${CMAKE_SOURCE_DIR}/share/THIRD_PARTY_NOTICES.txt + DESTINATION ${HYBBX_INSTALL_HOME} + ) + + install(DIRECTORY ${CMAKE_SOURCE_DIR}/share/reverse-proxy/ + DESTINATION ${HYBBX_INSTALL_HOME}/reverse-proxy + ) + + install(DIRECTORY ${CMAKE_SOURCE_DIR}/share/cgi/ + DESTINATION ${HYBBX_INSTALL_HOME}/cgi + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + ) + + install(FILES ${CMAKE_SOURCE_DIR}/share/data.gitkeep + DESTINATION ${HYBBX_INSTALL_HOME}/data + RENAME .gitkeep + ) + + install(FILES ${CMAKE_SOURCE_DIR}/share/logs.gitkeep + DESTINATION ${HYBBX_INSTALL_HOME}/logs + RENAME .gitkeep + ) + + install(DIRECTORY ${CMAKE_SOURCE_DIR}/share/fail2ban/ + DESTINATION ${HYBBX_INSTALL_HOME}/fail2ban + ) + + install(PROGRAMS ${CMAKE_SOURCE_DIR}/scripts/hybbx.sh + DESTINATION ${HYBBX_INSTALL_HOME} + RENAME hybbx-start + ) +endif() + +if(HYBBX_BUILD_CLIENTS) + add_subdirectory(clients) +endif() diff --git a/src/clients/CMakeLists.txt b/src/clients/CMakeLists.txt new file mode 100644 index 0000000..2181fbc --- /dev/null +++ b/src/clients/CMakeLists.txt @@ -0,0 +1,96 @@ +# Standalone CLI clients (hybbx-telnet, hybbx-terminal, hybbx-ssh). +# No centralized daemon; flags/env only — see src/clients/*.c for options. + +if(HYBBX_PLATFORM_AMIGAOS) + set(_hybbx_client_sources + ${CMAKE_SOURCE_DIR}/src/core/util.c + ${CMAKE_SOURCE_DIR}/src/core/traffic.c + ${CMAKE_SOURCE_DIR}/src/core/terminal.c + client_line.c + client_display.c + ) +else() + set(_hybbx_client_sources + ${CMAKE_SOURCE_DIR}/src/core/util.c + ${CMAKE_SOURCE_DIR}/src/core/circuit.c + ${CMAKE_SOURCE_DIR}/src/core/traffic.c + ${CMAKE_SOURCE_DIR}/src/core/terminal.c + ${CMAKE_SOURCE_DIR}/plugins/packet_radio/ax25.c + circuit_client.c + link_auth.c + client_line.c + client_display.c + ) +endif() + +add_library(hybbx_client STATIC ${_hybbx_client_sources}) + +target_include_directories(hybbx_client + PUBLIC + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/plugins/packet_radio +) + +target_compile_definitions(hybbx_client PRIVATE HYBBX_CLIENT_BUILD=1) +hybbx_apply_hardening(hybbx_client) + +if(HYBBX_PLATFORM_AMIGAOS) + target_compile_options(hybbx_client PRIVATE -mcrt=clib2) +endif() + +add_library(HyBBX::client ALIAS hybbx_client) + +if(HYBBX_BUILD_CLIENT_TELNET) + add_executable(hybbx-telnet + hybbx_telnet.c + ${CMAKE_SOURCE_DIR}/plugins/telnet/telnet_proto.c + ) + target_include_directories(hybbx-telnet PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/plugins/telnet + ) + target_link_libraries(hybbx-telnet PRIVATE hybbx_client) + hybbx_apply_hardening_executable(hybbx-telnet) + if(HYBBX_PLATFORM_AMIGAOS) + target_compile_options(hybbx-telnet PRIVATE -mcrt=clib2) + target_link_options(hybbx-telnet PRIVATE -mcrt=clib2) + target_link_libraries(hybbx-telnet PRIVATE net unix) + endif() + install(TARGETS hybbx-telnet RUNTIME DESTINATION ${HYBBX_INSTALL_HOME}) +endif() + +if(HYBBX_BUILD_CLIENT_TERMINAL) + add_executable(hybbx-terminal hybbx_terminal.c) + target_include_directories(hybbx-terminal PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/plugins/packet_radio + ) + target_link_libraries(hybbx-terminal PRIVATE hybbx_client) + hybbx_apply_hardening_executable(hybbx-terminal) + install(TARGETS hybbx-terminal RUNTIME DESTINATION ${HYBBX_INSTALL_HOME}) +endif() + +if(HYBBX_BUILD_CLIENT_SSH AND HYBBX_HAVE_LIBSSH) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh) + + add_executable(hybbx-ssh hybbx_ssh.c) + target_include_directories(hybbx-ssh PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ) + target_link_libraries(hybbx-ssh PRIVATE hybbx_client PkgConfig::LIBSSH) + hybbx_apply_hardening_executable(hybbx-ssh) + install(TARGETS hybbx-ssh RUNTIME DESTINATION ${HYBBX_INSTALL_HOME}) +endif() + +if(HYBBX_INSTALL_DEV) + install(TARGETS hybbx_client + EXPORT HyBBXTargets + ARCHIVE DESTINATION ${HYBBX_INSTALL_HOME}/lib + ) +endif() diff --git a/src/clients/circuit_client.c b/src/clients/circuit_client.c new file mode 100644 index 0000000..778ca87 --- /dev/null +++ b/src/clients/circuit_client.c @@ -0,0 +1,294 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit.h" +#include "hybbx/link.h" +#include "hybbx/socket.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_CIRCUIT_LINK_POLL_MS 50 +#define HYBBX_CIRCUIT_AUTH_TIMEOUT_MS 15000 + +static int set_socket_options(int fd) +{ + int on = 1; + int flags; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + return -1; + } + + return 0; +} + +hybbx_result_t hybbx_circuit_link_connect(const char *host, unsigned port, + int *out_fd) +{ + struct sockaddr_in6 addr6; + struct sockaddr_in addr4; + int fd; + int rc; + + if (host == NULL || out_fd == NULL || port == 0) { + return HYBBX_ERR_INVALID; + } + + if (strchr(host, ':') != NULL) { + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) { + return HYBBX_ERR_INVALID; + } + + fd = socket(AF_INET6, SOCK_STREAM, 0); + if (fd < 0) { + return HYBBX_ERR_IO; + } + + rc = connect(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) { + return HYBBX_ERR_INVALID; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return HYBBX_ERR_IO; + } + + rc = connect(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return HYBBX_ERR_IO; + } + + (void)set_socket_options(fd); + *out_fd = fd; + return HYBBX_OK; +} + +hybbx_result_t hybbx_circuit_link_write(int fd, const uint8_t *frame, + size_t len) +{ + ssize_t sent; + size_t off = 0; + + if (fd < 0 || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + while (off < len) { + sent = send(fd, frame + off, len - off, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)sent; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_circuit_link_read(int fd, uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + ssize_t n; + + if (fd < 0 || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + n = recv(fd, buf, buf_len, 0); + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + *read_len = 0; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + *read_len = 0; + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +typedef struct link_ack_wait { + int done; + int ok; +} link_ack_wait_t; + +static void on_link_auth_ack(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + link_ack_wait_t *wait = (link_ack_wait_t *)userdata; + size_t i; + + (void)flags; + + if (wait == NULL || wait->done || proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK) { + return; + } + + if (len == 0 || payload == NULL) { + return; + } + + for (i = 0; i + 6 <= len; i++) { + if (memcmp(payload + i, "ok=yes", 6) == 0) { + wait->ok = 1; + break; + } + } + wait->done = 1; +} + +hybbx_result_t hybbx_circuit_link_authenticate(int fd, + const char *password, + const char *role, + const char *id) +{ + return hybbx_circuit_link_authenticate_ex(fd, password, role, id, NULL); +} + +hybbx_result_t hybbx_circuit_link_authenticate_ex(int fd, + const char *password, + const char *role, + const char *id, + const hybbx_circuit_link_qos_t *qos) +{ + hybbx_link_auth_t auth; + char payload[HYBBX_LINK_AUTH_PAYLOAD_MAX]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t payload_len; + size_t frame_len; + hybbx_circuit_decoder_t dec; + link_ack_wait_t wait; + uint8_t buf[256]; + unsigned elapsed = 0; + + if (fd < 0 || password == NULL || id == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_link_auth_clear(&auth); + hybbx_strlcpy(auth.password, password, sizeof(auth.password)); + hybbx_strlcpy(auth.id, id, sizeof(auth.id)); + if (role != NULL && role[0] != '\0') { + hybbx_strlcpy(auth.role, role, sizeof(auth.role)); + } else { + hybbx_strlcpy(auth.role, "link", sizeof(auth.role)); + } + + if (qos != NULL) { + if (qos->baud > 0) { + auth.baud = qos->baud; + } + if (qos->duplex > 0) { + auth.duplex = qos->duplex; + } + if (qos->bandwidth != NULL && qos->bandwidth[0] != '\0') { + hybbx_strlcpy(auth.bandwidth, qos->bandwidth, sizeof(auth.bandwidth)); + } + if (qos->frequency_mhz != NULL && qos->frequency_mhz[0] != '\0') { + hybbx_strlcpy(auth.frequency_mhz, qos->frequency_mhz, + sizeof(auth.frequency_mhz)); + } + } + + payload_len = hybbx_link_auth_format(&auth, payload, sizeof(payload)); + if (payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_LINK_AUTH, + payload, payload_len, + frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_circuit_link_write(fd, frame, frame_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + hybbx_circuit_decoder_init(&dec); + memset(&wait, 0, sizeof(wait)); + + while (!wait.done && elapsed < HYBBX_CIRCUIT_AUTH_TIMEOUT_MS) { + struct pollfd pfd; + int pr; + size_t read_len; + + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (pr == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + if ((pfd.revents & POLLIN) == 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_circuit_link_read(fd, buf, sizeof(buf), &read_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (read_len == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + + hybbx_circuit_decoder_feed(&dec, buf, read_len, on_link_auth_ack, &wait); + } + + return wait.ok ? HYBBX_OK : HYBBX_ERR_DENIED; +} diff --git a/src/clients/client_display.c b/src/clients/client_display.c new file mode 100644 index 0000000..0f6a183 --- /dev/null +++ b/src/clients/client_display.c @@ -0,0 +1,87 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_display.h" + +#include "hybbx/terminal.h" +#include "hybbx/traffic.h" + +#include +#include + +void hybbx_client_display_init(hybbx_client_display_t *disp, + const hybbx_traffic_config_t *traffic) +{ + if (disp == NULL) { + return; + } + + disp->traffic = traffic != NULL ? traffic : hybbx_traffic_config_get(); + disp->col = 0; +} + +static void pace_byte(unsigned baud) +{ + unsigned delay_us; + + if (baud == 0) { + return; + } + + delay_us = hybbx_traffic_byte_delay_us(baud); + if (delay_us > 0) { + usleep(delay_us); + } +} + +void hybbx_client_display_byte(hybbx_client_display_t *disp, uint8_t byte) +{ + const hybbx_traffic_config_t *cfg; + + if (disp == NULL) { + return; + } + + cfg = disp->traffic; + if (cfg == NULL) { + fputc((int)byte, stdout); + fflush(stdout); + return; + } + + if (!cfg->ansi && byte == 0x1b) { + return; + } + + if (cfg->line_width > 0 && (byte == '\n' || byte == '\r')) { + disp->col = 0; + } else if (cfg->line_width > 0 && byte >= 0x20 && byte != 0x7f) { + disp->col++; + if (disp->col > cfg->line_width) { + fputc('\n', stdout); + disp->col = 1; + } + } + + fputc((int)byte, stdout); + fflush(stdout); + + if (cfg->pace_output) { + pace_byte(cfg->baud); + } +} + +void hybbx_client_display_write(hybbx_client_display_t *disp, + const uint8_t *data, size_t len) +{ + size_t i; + + if (disp == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + hybbx_client_display_byte(disp, data[i]); + } +} diff --git a/src/clients/client_display.h b/src/clients/client_display.h new file mode 100644 index 0000000..a457f4c --- /dev/null +++ b/src/clients/client_display.h @@ -0,0 +1,30 @@ +#ifndef HYBBX_CLIENT_DISPLAY_H +#define HYBBX_CLIENT_DISPLAY_H + +#include "hybbx/traffic.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_client_display { + const hybbx_traffic_config_t *traffic; + unsigned col; +} hybbx_client_display_t; + +void hybbx_client_display_init(hybbx_client_display_t *disp, + const hybbx_traffic_config_t *traffic); + +void hybbx_client_display_byte(hybbx_client_display_t *disp, uint8_t byte); + +void hybbx_client_display_write(hybbx_client_display_t *disp, + const uint8_t *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CLIENT_DISPLAY_H */ diff --git a/src/clients/client_line.c b/src/clients/client_line.c new file mode 100644 index 0000000..ab8c07b --- /dev/null +++ b/src/clients/client_line.c @@ -0,0 +1,495 @@ +#include "client_line.h" + +#include "hybbx/util.h" + +#include +#include +#include +#include + +#define CLIENT_ESC_NONE 0u +#define CLIENT_ESC_ESC 1u +#define CLIENT_ESC_CSI 2u +#define CLIENT_ESC_SS3 3u + +static int history_should_store(const char *line) +{ + if (line == NULL || line[0] == '\0') { + return 0; + } + + return strncmp(line, "/login ", 7) != 0 && + strncmp(line, "/register ", 10) != 0 && + strncmp(line, "/changeme ", 10) != 0 && + strncmp(line, "/userchange ", 12) != 0; +} + +static hybbx_result_t write_all(int fd, const char *buf, size_t len) +{ + size_t off = 0; + + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)n; + } + + return HYBBX_OK; +} + +static hybbx_result_t move_back(hybbx_client_line_editor_t *ed, size_t count) +{ + char buf[64]; + size_t sent = 0; + + if (ed == NULL || !ed->raw_enabled || count == 0) { + return HYBBX_OK; + } + + memset(buf, '\b', sizeof(buf)); + while (sent < count) { + size_t chunk = count - sent; + + if (chunk > sizeof(buf)) { + chunk = sizeof(buf); + } + if (write_all(STDOUT_FILENO, buf, chunk) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + sent += chunk; + } + + return HYBBX_OK; +} + +static hybbx_result_t refresh_suffix(hybbx_client_line_editor_t *ed) +{ + size_t suffix_len; + + if (ed == NULL || !ed->raw_enabled) { + return HYBBX_OK; + } + + suffix_len = ed->line_len - ed->line_cursor; + if (suffix_len > 0 && + write_all(STDOUT_FILENO, ed->line + ed->line_cursor, suffix_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (write_all(STDOUT_FILENO, "\x1b[K", 3) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + return move_back(ed, suffix_len); +} + +static hybbx_result_t cursor_left(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor == 0) { + return HYBBX_OK; + } + ed->line_cursor--; + return ed->raw_enabled ? write_all(STDOUT_FILENO, "\b", 1) : HYBBX_OK; +} + +static hybbx_result_t cursor_right(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor >= ed->line_len) { + return HYBBX_OK; + } + if (ed->raw_enabled && + write_all(STDOUT_FILENO, ed->line + ed->line_cursor, 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + ed->line_cursor++; + return HYBBX_OK; +} + +static hybbx_result_t cursor_home(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL) { + return HYBBX_ERR_INVALID; + } + return move_back(ed, ed->line_cursor); +} + +static hybbx_result_t cursor_end(hybbx_client_line_editor_t *ed) +{ + while (ed != NULL && ed->line_cursor < ed->line_len) { + if (cursor_right(ed) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + return HYBBX_OK; +} + +static hybbx_result_t delete_forward(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor >= ed->line_len) { + return HYBBX_OK; + } + + memmove(ed->line + ed->line_cursor, ed->line + ed->line_cursor + 1, + ed->line_len - ed->line_cursor); + ed->line_len--; + ed->line[ed->line_len] = '\0'; + return refresh_suffix(ed); +} + +static hybbx_result_t backspace_char(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || ed->line_cursor == 0) { + return HYBBX_OK; + } + + ed->line_cursor--; + memmove(ed->line + ed->line_cursor, ed->line + ed->line_cursor + 1, + ed->line_len - ed->line_cursor); + ed->line_len--; + ed->line[ed->line_len] = '\0'; + if (ed->raw_enabled && write_all(STDOUT_FILENO, "\b", 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + return refresh_suffix(ed); +} + +static hybbx_result_t insert_char(hybbx_client_line_editor_t *ed, char ch) +{ + if (ed == NULL || (unsigned char)ch < 0x20u || ch == 0x7f) { + return HYBBX_OK; + } + if (ed->line_len + 1 >= sizeof(ed->line)) { + return HYBBX_OK; + } + + if (ed->line_cursor < ed->line_len) { + memmove(ed->line + ed->line_cursor + 1, ed->line + ed->line_cursor, + ed->line_len - ed->line_cursor); + } + ed->line[ed->line_cursor] = ch; + ed->line_len++; + ed->line_cursor++; + ed->line[ed->line_len] = '\0'; + + if (!ed->raw_enabled) { + return HYBBX_OK; + } + if (write_all(STDOUT_FILENO, &ch, 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + return refresh_suffix(ed); +} + +static void history_add(hybbx_client_line_editor_t *ed, const char *line) +{ + if (ed == NULL || !history_should_store(line)) { + return; + } + + hybbx_strlcpy(ed->history[ed->history_next], line, sizeof(ed->history[0])); + ed->history_next = (ed->history_next + 1u) % HYBBX_HISTORY_MAX; + if (ed->history_count < HYBBX_HISTORY_MAX) { + ed->history_count++; + } + ed->history_view = -1; + ed->history_saved_line[0] = '\0'; +} + +static hybbx_result_t line_replace(hybbx_client_line_editor_t *ed, const char *line) +{ + size_t len; + + if (ed == NULL || line == NULL) { + return HYBBX_ERR_INVALID; + } + + len = strlen(line); + if (len >= sizeof(ed->line)) { + len = sizeof(ed->line) - 1; + } + + if (ed->raw_enabled) { + if (move_back(ed, ed->line_cursor) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (len > 0 && write_all(STDOUT_FILENO, line, len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (write_all(STDOUT_FILENO, "\x1b[K", 3) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + memcpy(ed->line, line, len); + ed->line[len] = '\0'; + ed->line_len = len; + ed->line_cursor = len; + return HYBBX_OK; +} + +static hybbx_result_t history_up(hybbx_client_line_editor_t *ed) +{ + unsigned slot; + + if (ed == NULL || ed->history_count == 0) { + return HYBBX_OK; + } + + if (ed->history_view < 0) { + hybbx_strlcpy(ed->history_saved_line, ed->line, sizeof(ed->history_saved_line)); + ed->history_view = 0; + } else if ((unsigned)(ed->history_view + 1) < ed->history_count) { + ed->history_view++; + } else { + return HYBBX_OK; + } + + slot = (ed->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)ed->history_view) % HYBBX_HISTORY_MAX; + return line_replace(ed, ed->history[slot]); +} + +static hybbx_result_t history_down(hybbx_client_line_editor_t *ed) +{ + unsigned slot; + + if (ed == NULL || ed->history_view < 0) { + return HYBBX_OK; + } + if (ed->history_view == 0) { + ed->history_view = -1; + return line_replace(ed, ed->history_saved_line); + } + + ed->history_view--; + slot = (ed->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)ed->history_view) % HYBBX_HISTORY_MAX; + return line_replace(ed, ed->history[slot]); +} + +static void escape_reset(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL) { + return; + } + ed->esc_state = CLIENT_ESC_NONE; + ed->csi_len = 0; +} + +static int csi_is_final(unsigned char ch) +{ + return ch >= 0x40u && ch <= 0x7eu; +} + +static hybbx_result_t apply_csi(hybbx_client_line_editor_t *ed, + const char *params, char final_ch) +{ + (void)params; + + if (final_ch == 'A') { + return history_up(ed); + } + if (final_ch == 'B') { + return history_down(ed); + } + if (final_ch == 'C') { + return cursor_right(ed); + } + if (final_ch == 'D') { + return cursor_left(ed); + } + if (final_ch == 'H') { + return cursor_home(ed); + } + if (final_ch == 'F') { + return cursor_end(ed); + } + if (final_ch == '~') { + if (strcmp(params, "1") == 0 || strcmp(params, "7") == 0) { + return cursor_home(ed); + } + if (strcmp(params, "3") == 0) { + return delete_forward(ed); + } + if (strcmp(params, "4") == 0 || strcmp(params, "8") == 0) { + return cursor_end(ed); + } + } + return HYBBX_OK; +} + +static int filter_escape_byte(hybbx_client_line_editor_t *ed, unsigned char byte) +{ + if (ed == NULL) { + return 0; + } + if (ed->esc_state == CLIENT_ESC_NONE && byte == 0x9bu) { + ed->esc_state = CLIENT_ESC_CSI; + ed->csi_len = 0; + return 1; + } + if (ed->esc_state == CLIENT_ESC_NONE && byte == 0x1bu) { + ed->esc_state = CLIENT_ESC_ESC; + return 1; + } + if (ed->esc_state == CLIENT_ESC_ESC) { + if (byte == '[') { + ed->esc_state = CLIENT_ESC_CSI; + ed->csi_len = 0; + return 1; + } + if (byte == 'O') { + ed->esc_state = CLIENT_ESC_SS3; + return 1; + } + escape_reset(ed); + return 0; + } + if (ed->esc_state == CLIENT_ESC_SS3) { + if (byte == 'C') { + (void)cursor_right(ed); + } else if (byte == 'D') { + (void)cursor_left(ed); + } else if (byte == 'H') { + (void)cursor_home(ed); + } else if (byte == 'F') { + (void)cursor_end(ed); + } + escape_reset(ed); + return 1; + } + if (ed->esc_state == CLIENT_ESC_CSI) { + char params[24]; + + if (ed->csi_len + 1 >= sizeof(ed->csi_buf)) { + escape_reset(ed); + return 1; + } + ed->csi_buf[ed->csi_len++] = (char)byte; + if (!csi_is_final(byte)) { + return 1; + } + + if (ed->csi_len > 1) { + memcpy(params, ed->csi_buf, ed->csi_len - 1); + params[ed->csi_len - 1] = '\0'; + } else { + params[0] = '\0'; + } + escape_reset(ed); + (void)apply_csi(ed, params, (char)byte); + return 1; + } + return 0; +} + +int hybbx_client_line_editor_start(hybbx_client_line_editor_t *ed, int stdin_fd) +{ + struct termios raw; + + if (ed == NULL) { + return -1; + } + + memset(ed, 0, sizeof(*ed)); + ed->stdin_fd = stdin_fd; + ed->history_view = -1; + + if (!isatty(stdin_fd)) { + return 0; + } + if (tcgetattr(stdin_fd, &ed->termios_saved) != 0) { + return -1; + } + + raw = ed->termios_saved; + raw.c_iflag &= ~(ICRNL | IXON); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + + if (tcsetattr(stdin_fd, TCSAFLUSH, &raw) != 0) { + return -1; + } + + ed->raw_enabled = 1; + return 0; +} + +void hybbx_client_line_editor_stop(hybbx_client_line_editor_t *ed) +{ + if (ed == NULL || !ed->raw_enabled) { + return; + } + (void)tcsetattr(ed->stdin_fd, TCSAFLUSH, &ed->termios_saved); + ed->raw_enabled = 0; +} + +hybbx_result_t hybbx_client_line_editor_poll(hybbx_client_line_editor_t *ed, + char *out_line, size_t out_len, + int *have_line, int *eof_seen) +{ + unsigned char buf[64]; + ssize_t n; + size_t i; + + if (ed == NULL || out_line == NULL || out_len == 0 || + have_line == NULL || eof_seen == NULL) { + return HYBBX_ERR_INVALID; + } + + *have_line = 0; + *eof_seen = 0; + + n = read(ed->stdin_fd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EINTR) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + *eof_seen = 1; + return HYBBX_OK; + } + + for (i = 0; i < (size_t)n; i++) { + unsigned char ch = buf[i]; + + if (filter_escape_byte(ed, ch)) { + continue; + } + if (ch == '\b' || ch == 127) { + if (backspace_char(ed) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + continue; + } + if (ch == '\r' || ch == '\n') { + if (ed->raw_enabled && + write_all(STDOUT_FILENO, "\r\n", 2) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + hybbx_strlcpy(out_line, ed->line, out_len); + history_add(ed, ed->line); + ed->line[0] = '\0'; + ed->line_len = 0; + ed->line_cursor = 0; + ed->history_view = -1; + *have_line = 1; + return HYBBX_OK; + } + if (insert_char(ed, (char)ch) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} diff --git a/src/clients/client_line.h b/src/clients/client_line.h new file mode 100644 index 0000000..928efe0 --- /dev/null +++ b/src/clients/client_line.h @@ -0,0 +1,33 @@ +#ifndef HYBBX_CLIENT_LINE_H +#define HYBBX_CLIENT_LINE_H + +#include "hybbx/limits.h" +#include "hybbx/types.h" + +#include +#include + +typedef struct hybbx_client_line_editor { + char line[HYBBX_LINE_MAX]; + size_t line_len; + size_t line_cursor; + char history[HYBBX_HISTORY_MAX][HYBBX_LINE_MAX]; + unsigned history_count; + unsigned history_next; + int history_view; + char history_saved_line[HYBBX_LINE_MAX]; + int raw_enabled; + int stdin_fd; + unsigned char esc_state; + char csi_buf[24]; + size_t csi_len; + struct termios termios_saved; +} hybbx_client_line_editor_t; + +int hybbx_client_line_editor_start(hybbx_client_line_editor_t *ed, int stdin_fd); +void hybbx_client_line_editor_stop(hybbx_client_line_editor_t *ed); +hybbx_result_t hybbx_client_line_editor_poll(hybbx_client_line_editor_t *ed, + char *out_line, size_t out_len, + int *have_line, int *eof_seen); + +#endif /* HYBBX_CLIENT_LINE_H */ diff --git a/src/clients/hybbx_ssh.c b/src/clients/hybbx_ssh.c new file mode 100644 index 0000000..c2d6d88 --- /dev/null +++ b/src/clients/hybbx_ssh.c @@ -0,0 +1,495 @@ +/** + * hybbx-ssh — HyBBX-native SSH client (CLI only, no GUI). + * + * Encrypted transport to HyBBX [transport.ssh] (default port 3232). + * Wire SSH username/password satisfy the libssh handshake only; HyBBX + * accounts use guest auto-login or /login like telnet. + * Host keys are not verified — no known_hosts prompt. + */ + +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_line.h" +#include "client_display.h" +#include "hybbx/limits.h" +#include "hybbx/ssh.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_SSH_POLL_MS 50 +#define HYBBX_SSH_WIRE_USER "hybbx" +#define HYBBX_SSH_WIRE_PASSWORD "hybbx" + +typedef struct hybbx_ssh_client_config { + char host[256]; + unsigned port; + char wire_user[64]; + char wire_password[128]; + char login_user[64]; + char login_password[128]; + unsigned baud; + unsigned line_width; + int pace_output; + int ansi; + int verbose; + unsigned timeout_sec; + int ipv6; +} hybbx_ssh_client_config_t; + +typedef struct ssh_session_ctx { + ssh_session session; + ssh_channel channel; + hybbx_client_display_t display; + hybbx_client_line_editor_t editor; + int editor_ready; +} ssh_session_ctx_t; + +static void config_defaults(hybbx_ssh_client_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + hybbx_strlcpy(cfg->host, "127.0.0.1", sizeof(cfg->host)); + cfg->port = HYBBX_SSH_DEFAULT_PORT; + hybbx_strlcpy(cfg->wire_user, HYBBX_SSH_WIRE_USER, sizeof(cfg->wire_user)); + hybbx_strlcpy(cfg->wire_password, HYBBX_SSH_WIRE_PASSWORD, + sizeof(cfg->wire_password)); + cfg->baud = HYBBX_BAUD2400; + cfg->line_width = HYBBX_LINE_WIDTH; + cfg->pace_output = 1; + cfg->timeout_sec = 30; +} + +static void print_usage(const char *prog) +{ + fprintf(stderr, + "hybbx-ssh — HyBBX SSH client (encrypted transport, CLI only)\n" + "\n" + "Usage: %s [options] [host [port]]\n" + "\n" + "Options:\n" + " -H, --host HOST Remote host (default 127.0.0.1)\n" + " -p, --port PORT SSH port (default 3232)\n" + " -u, --user USER Send /login USER after connect\n" + " -P, --password PASS Password for /login\n" + " --wire-user USER SSH wire username (default hybbx)\n" + " --wire-pass PASS SSH wire password (default hybbx)\n" + " --baud N Pace output like HyBBX (default 2400, 0=off)\n" + " --line-width N Wrap display columns (default 80, 0=off)\n" + " --pace yes|no Output pacing (default yes)\n" + " --ansi yes|no Pass ANSI sequences (default no)\n" + " -6, --ipv6 Prefer IPv6 when connecting\n" + " -t, --timeout SEC Connect timeout (default 30)\n" + " -v, --verbose Log SSH progress on stderr\n" + " -h, --help Show help\n" + "\n" + "SSH wire credentials are not HyBBX accounts — any value is accepted\n" + "by the server. Use -u/-P for HyBBX /login after connect.\n" + "\n" + "Environment: HYBBX_HOST, HYBBX_PORT, HYBBX_SSH_WIRE_USER,\n" + " HYBBX_SSH_WIRE_PASS\n" + "\n", + prog); +} + +static void log_ssh_error(ssh_session session, const char *what) +{ + const char *msg = ssh_get_error(session); + + if (msg == NULL || msg[0] == '\0') { + msg = "unknown error"; + } + fprintf(stderr, "hybbx-ssh: %s: %s\n", what, msg); +} + +static int ssh_connect_session(const hybbx_ssh_client_config_t *cfg, + ssh_session *out_session, + ssh_channel *out_channel) +{ + ssh_session session; + ssh_channel channel; + char portbuf[16]; + long timeout_sec; + int strict = 0; + int process_config = 0; + int pubkey_auth = 0; + int nodelay = 1; + int rc; + + session = ssh_new(); + if (session == NULL) { + fprintf(stderr, "hybbx-ssh: ssh_new failed\n"); + return -1; + } + + snprintf(portbuf, sizeof(portbuf), "%u", cfg->port); + timeout_sec = (long)cfg->timeout_sec; + + (void)ssh_options_set(session, SSH_OPTIONS_HOST, cfg->host); + (void)ssh_options_set(session, SSH_OPTIONS_PORT_STR, portbuf); + (void)ssh_options_set(session, SSH_OPTIONS_USER, cfg->wire_user); + (void)ssh_options_set(session, SSH_OPTIONS_STRICTHOSTKEYCHECK, &strict); + (void)ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, &process_config); + (void)ssh_options_set(session, SSH_OPTIONS_PUBKEY_AUTH, &pubkey_auth); + (void)ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &timeout_sec); + (void)ssh_options_set(session, SSH_OPTIONS_NODELAY, &nodelay); + + if (cfg->ipv6) { + (void)ssh_options_set(session, SSH_OPTIONS_BINDADDR, "::"); + } + + rc = ssh_connect(session); + if (rc != SSH_OK) { + log_ssh_error(session, "connect"); + ssh_free(session); + return -1; + } + + if (cfg->verbose) { + fprintf(stderr, "hybbx-ssh: SSH handshake complete with %s:%u\n", + cfg->host, cfg->port); + } + + rc = ssh_userauth_password(session, NULL, cfg->wire_password); + if (rc != SSH_AUTH_SUCCESS) { + log_ssh_error(session, "wire authentication"); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + channel = ssh_channel_new(session); + if (channel == NULL) { + log_ssh_error(session, "channel_new"); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + rc = ssh_channel_open_session(channel); + if (rc != SSH_OK) { + log_ssh_error(session, "channel_open_session"); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + rc = ssh_channel_request_pty(channel); + if (rc != SSH_OK) { + log_ssh_error(session, "channel_request_pty"); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + rc = ssh_channel_request_shell(channel); + if (rc != SSH_OK) { + log_ssh_error(session, "channel_request_shell"); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + return -1; + } + + ssh_set_blocking(session, 0); + + *out_session = session; + *out_channel = channel; + return 0; +} + +static hybbx_result_t send_line(ssh_channel channel, const char *line) +{ + char buf[HYBBX_LINE_MAX + 2]; + size_t len; + int rc; + + snprintf(buf, sizeof(buf), "%s\n", line); + len = strlen(buf); + + rc = ssh_channel_write(channel, buf, (uint32_t)len); + if (rc < 0 || (size_t)rc != len) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void maybe_auto_login(ssh_channel channel, + const hybbx_ssh_client_config_t *cfg) +{ + char line[192]; + + if (cfg->login_user[0] == '\0') { + return; + } + + snprintf(line, sizeof(line), "/login %s", cfg->login_user); + (void)send_line(channel, line); + if (cfg->login_password[0] != '\0') { + (void)send_line(channel, cfg->login_password); + } +} + +static int drain_channel(ssh_session_ctx_t *ctx, int verbose) +{ + uint8_t buf[512]; + int n; + + for (;;) { + n = ssh_channel_read_nonblocking(ctx->channel, buf, sizeof(buf), 0); + if (n == SSH_AGAIN) { + return 0; + } + if (n <= 0) { + if (ssh_channel_is_eof(ctx->channel) || ssh_channel_is_closed(ctx->channel)) { + return -1; + } + return 0; + } + + if (verbose) { + fprintf(stderr, "hybbx-ssh: recv %d bytes\n", n); + } + + hybbx_client_display_write(&ctx->display, buf, (size_t)n); + } +} + +static int run_session(ssh_session_ctx_t *ctx, const hybbx_ssh_client_config_t *cfg) +{ + char inbuf[HYBBX_LINE_MAX + 2]; + int running = 1; + + maybe_auto_login(ctx->channel, cfg); + + while (running) { + struct pollfd pfds[2]; + socket_t ssh_fd; + int pr; + int nready = 1; + + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + + ssh_fd = ssh_get_fd(ctx->session); + pfds[1].fd = (int)ssh_fd; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + nready = 2; + + pr = poll(pfds, (unsigned int)nready, HYBBX_SSH_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (drain_channel(ctx, cfg->verbose) != 0) { + break; + } + + if ((pfds[0].revents & POLLIN) != 0) { + int have_line = 0; + int eof_seen = 0; + + if (!ctx->editor_ready || + hybbx_client_line_editor_poll(&ctx->editor, inbuf, sizeof(inbuf), + &have_line, &eof_seen) != HYBBX_OK) { + running = 0; + } else if (eof_seen) { + running = 0; + } else if (have_line) { + if (send_line(ctx->channel, inbuf) != HYBBX_OK) { + break; + } + } + } + + if ((pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + + if ((pfds[1].revents & POLLIN) != 0 && drain_channel(ctx, cfg->verbose) != 0) { + break; + } + } + + return 0; +} + +static void session_ctx_init(ssh_session_ctx_t *ctx, + ssh_session session, + ssh_channel channel, + const hybbx_ssh_client_config_t *cfg) +{ + hybbx_traffic_config_t traffic; + + memset(ctx, 0, sizeof(*ctx)); + ctx->session = session; + ctx->channel = channel; + + hybbx_traffic_config_defaults(&traffic); + traffic.baud = cfg->baud; + traffic.line_width = cfg->line_width; + traffic.pace_output = cfg->pace_output; + traffic.ansi = cfg->ansi; + hybbx_client_display_init(&ctx->display, &traffic); + + if (hybbx_client_line_editor_start(&ctx->editor, STDIN_FILENO) == 0) { + ctx->editor_ready = 1; + } +} + +static void session_ctx_fini(ssh_session_ctx_t *ctx) +{ + if (ctx->editor_ready) { + hybbx_client_line_editor_stop(&ctx->editor); + ctx->editor_ready = 0; + } +} + +int main(int argc, char *argv[]) +{ + hybbx_ssh_client_config_t cfg; + ssh_session session = NULL; + ssh_channel channel = NULL; + ssh_session_ctx_t ctx; + int opt; + int argi = 1; + const char *env; + + static const struct option long_opts[] = { + {"host", required_argument, NULL, 'H'}, + {"port", required_argument, NULL, 'p'}, + {"user", required_argument, NULL, 'u'}, + {"password", required_argument, NULL, 'P'}, + {"wire-user", required_argument, NULL, 1004}, + {"wire-pass", required_argument, NULL, 1005}, + {"baud", required_argument, NULL, 1000}, + {"line-width", required_argument, NULL, 1001}, + {"pace", required_argument, NULL, 1002}, + {"ansi", required_argument, NULL, 1003}, + {"ipv6", no_argument, NULL, '6'}, + {"timeout", required_argument, NULL, 't'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + config_defaults(&cfg); + + env = getenv("HYBBX_HOST"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.host, env, sizeof(cfg.host)); + } + env = getenv("HYBBX_PORT"); + if (env != NULL && env[0] != '\0') { + cfg.port = (unsigned)strtoul(env, NULL, 10); + } + env = getenv("HYBBX_SSH_WIRE_USER"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.wire_user, env, sizeof(cfg.wire_user)); + } + env = getenv("HYBBX_SSH_WIRE_PASS"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.wire_password, env, sizeof(cfg.wire_password)); + } + + while ((opt = getopt_long(argc, argv, "H:p:u:P:6t:vh", long_opts, NULL)) != -1) { + switch (opt) { + case 'H': + hybbx_strlcpy(cfg.host, optarg, sizeof(cfg.host)); + break; + case 'p': + cfg.port = (unsigned)strtoul(optarg, NULL, 10); + break; + case 'u': + hybbx_strlcpy(cfg.login_user, optarg, sizeof(cfg.login_user)); + break; + case 'P': + hybbx_strlcpy(cfg.login_password, optarg, sizeof(cfg.login_password)); + break; + case '6': + cfg.ipv6 = 1; + break; + case 't': + cfg.timeout_sec = (unsigned)strtoul(optarg, NULL, 10); + break; + case 'v': + cfg.verbose = 1; + break; + case 'h': + print_usage(argv[0]); + return EXIT_SUCCESS; + case 1000: + cfg.baud = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1001: + cfg.line_width = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1002: + cfg.pace_output = hybbx_parse_bool(optarg, cfg.pace_output); + break; + case 1003: + cfg.ansi = hybbx_parse_bool(optarg, cfg.ansi); + break; + case 1004: + hybbx_strlcpy(cfg.wire_user, optarg, sizeof(cfg.wire_user)); + break; + case 1005: + hybbx_strlcpy(cfg.wire_password, optarg, sizeof(cfg.wire_password)); + break; + default: + print_usage(argv[0]); + return EXIT_FAILURE; + } + } + + argi = optind; + if (argi < argc) { + hybbx_strlcpy(cfg.host, argv[argi++], sizeof(cfg.host)); + } + if (argi < argc) { + cfg.port = (unsigned)strtoul(argv[argi++], NULL, 10); + } + + if (ssh_connect_session(&cfg, &session, &channel) != 0) { + return EXIT_FAILURE; + } + + if (cfg.verbose) { + fprintf(stderr, + "hybbx-ssh: shell ready on %s:%u (baud=%u width=%u wire=%s)\n", + cfg.host, cfg.port, cfg.baud, cfg.line_width, cfg.wire_user); + } + + session_ctx_init(&ctx, session, channel, &cfg); + run_session(&ctx, &cfg); + session_ctx_fini(&ctx); + + ssh_channel_send_eof(channel); + ssh_channel_close(channel); + ssh_channel_free(channel); + ssh_disconnect(session); + ssh_free(session); + + return EXIT_SUCCESS; +} diff --git a/src/clients/hybbx_telnet.c b/src/clients/hybbx_telnet.c new file mode 100644 index 0000000..c19e747 --- /dev/null +++ b/src/clients/hybbx_telnet.c @@ -0,0 +1,544 @@ +/** + * hybbx-telnet — HyBBX-native telnet client (CLI only, no GUI). + * + * Full telnet negotiation support for testing HyBBX servers. + * Optional 2400 baud pacing and 80-column display profile. + */ + +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_line.h" +#include "client_display.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" +#include "telnet_proto.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__AMIGA__) +#include +#include +#include +#include +#else +#include +#include +#endif + +#define HYBBX_TELNET_DEFAULT_PORT 2323u +#define HYBBX_TELNET_POLL_MS 50 + +typedef struct hybbx_telnet_client_config { + char host[256]; + unsigned port; + char login_user[64]; + char login_password[128]; + unsigned baud; + unsigned line_width; + int pace_output; + int ansi; + int verbose; + unsigned timeout_sec; + int ipv6; +} hybbx_telnet_client_config_t; + +static void config_defaults(hybbx_telnet_client_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + hybbx_strlcpy(cfg->host, "127.0.0.1", sizeof(cfg->host)); + cfg->port = HYBBX_TELNET_DEFAULT_PORT; + cfg->baud = HYBBX_BAUD2400; + cfg->line_width = HYBBX_LINE_WIDTH; + cfg->pace_output = 1; + cfg->timeout_sec = 30; +} + +static void print_usage(const char *prog) +{ + fprintf(stderr, + "hybbx-telnet — HyBBX telnet client for testing (CLI only)\n" + "\n" + "Usage: %s [options] [host [port]]\n" + "\n" + "Options:\n" + " -H, --host HOST Remote host (default 127.0.0.1)\n" + " -p, --port PORT Telnet port (default 2323)\n" + " -u, --user USER Send /login USER after connect\n" + " -P, --password PASS Password for /login\n" + " --baud N Pace output like HyBBX (default 2400, 0=off)\n" + " --line-width N Wrap display columns (default 80, 0=off)\n" + " --pace yes|no Output pacing (default yes)\n" + " --ansi yes|no Pass ANSI sequences (default no)\n" + " -6, --ipv6 Prefer IPv6 when resolving host\n" + " -t, --timeout SEC Connect timeout (default 30)\n" + " -v, --verbose Log telnet negotiation on stderr\n" + " -h, --help Show help\n" + "\n" + "Environment: HYBBX_HOST, HYBBX_PORT\n" + "\n", + prog); +} + +static int tcp_connect(const hybbx_telnet_client_config_t *cfg, int *out_fd) +{ +#if defined(__AMIGA__) + struct sockaddr_in addr; + struct hostent *he; + int fd; + int on = 1; + + if (cfg->ipv6) { + fprintf(stderr, "hybbx-telnet: IPv6 is not supported on AmigaOS\n"); + return -1; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + fprintf(stderr, "hybbx-telnet: socket failed for %s:%u\n", + cfg->host, cfg->port); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)cfg->port); + + if (inet_aton(cfg->host, &addr.sin_addr) == 0) { + he = gethostbyname(cfg->host); + if (he == NULL || he->h_addrtype != AF_INET || he->h_length == 0) { + fprintf(stderr, "hybbx-telnet: unknown host %s\n", cfg->host); + close(fd); + return -1; + } + memcpy(&addr.sin_addr, he->h_addr, (size_t)he->h_length); + } + + (void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); + hybbx_socket_nosigpipe(fd); + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + fprintf(stderr, "hybbx-telnet: could not connect to %s:%u\n", + cfg->host, cfg->port); + close(fd); + return -1; + } + + *out_fd = fd; + return 0; +#else + char portbuf[16]; + struct addrinfo hints; + struct addrinfo *res = NULL; + struct addrinfo *ai; + int fd = -1; + int rc; + + snprintf(portbuf, sizeof(portbuf), "%u", cfg->port); + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = cfg->ipv6 ? AF_INET6 : AF_UNSPEC; + + rc = getaddrinfo(cfg->host, portbuf, &hints, &res); + if (rc != 0) { + fprintf(stderr, "hybbx-telnet: connect %s:%u failed: %s\n", + cfg->host, cfg->port, gai_strerror(rc)); + return -1; + } + + for (ai = res; ai != NULL; ai = ai->ai_next) { + int on = 1; + + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) { + continue; + } + + (void)setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); + hybbx_socket_nosigpipe(fd); + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) { + break; + } + close(fd); + fd = -1; + } + + freeaddrinfo(res); + + if (fd < 0) { + fprintf(stderr, "hybbx-telnet: could not connect to %s:%u\n", + cfg->host, cfg->port); + return -1; + } + + *out_fd = fd; + return 0; +#endif +} + +typedef struct display_ctx { + hybbx_client_display_t display; +} display_ctx_t; + +static void on_telnet_data(void *userdata, const uint8_t *data, size_t len) +{ + display_ctx_t *ctx = (display_ctx_t *)userdata; + + hybbx_client_display_write(&ctx->display, data, len); +} + +static hybbx_result_t send_raw(int fd, const uint8_t *data, size_t len) +{ + size_t off = 0; + + while (off < len) { + ssize_t n = send(fd, data + off, len - off, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)n; + } + + return HYBBX_OK; +} + +static hybbx_result_t send_line(int fd, const char *line) +{ + size_t len = strlen(line); + uint8_t stack[256]; + uint8_t *buf = stack; + size_t cap = sizeof(stack); + size_t need = len; + size_t i; + size_t pos = 0; + hybbx_result_t rc; + + for (i = 0; i < len; i++) { + if (line[i] == (char)255) { + need++; + } + } + need += 1; + + if (need > cap) { + buf = malloc(need); + if (buf == NULL) { + return HYBBX_ERR_NOMEM; + } + cap = need; + } + + for (i = 0; i < len; i++) { + buf[pos++] = (uint8_t)line[i]; + if (line[i] == (char)255) { + buf[pos++] = 255; + } + } + buf[pos++] = (uint8_t)'\n'; + + rc = send_raw(fd, buf, pos); + if (buf != stack) { + free(buf); + } + return rc; +} + +static void maybe_auto_login(int fd, const hybbx_telnet_client_config_t *cfg) +{ + char line[192]; + + if (cfg->login_user[0] == '\0') { + return; + } + + snprintf(line, sizeof(line), "/login %s", cfg->login_user); + (void)send_line(fd, line); + if (cfg->login_password[0] != '\0') { + (void)send_line(fd, cfg->login_password); + } +} + +static int run_session(int fd, const hybbx_telnet_client_config_t *cfg) +{ + hybbx_telnet_parser_t parser; + display_ctx_t display; + hybbx_client_line_editor_t editor; + hybbx_traffic_config_t traffic; + char inbuf[HYBBX_LINE_MAX + 2]; + int running = 1; + int editor_ready = 0; + + hybbx_traffic_config_defaults(&traffic); + traffic.baud = cfg->baud; + traffic.line_width = cfg->line_width; + traffic.pace_output = cfg->pace_output; + traffic.ansi = cfg->ansi; + hybbx_client_display_init(&display.display, &traffic); + hybbx_telnet_parser_init(&parser); + if (hybbx_client_line_editor_start(&editor, STDIN_FILENO) == 0) { + editor_ready = 1; + } + + (void)hybbx_telnet_send_greeting(fd); + maybe_auto_login(fd, cfg); + + while (running) { +#if defined(__AMIGA__) + fd_set rfds; + struct timeval tv; + int maxfd; + int pr; + + FD_ZERO(&rfds); + FD_SET(STDIN_FILENO, &rfds); + FD_SET(fd, &rfds); + maxfd = fd; + if (STDIN_FILENO > maxfd) { + maxfd = STDIN_FILENO; + } + tv.tv_sec = HYBBX_TELNET_POLL_MS / 1000; +#if defined(__AMIGA__) + tv.tv_usec = (unsigned long)((HYBBX_TELNET_POLL_MS % 1000) * 1000); +#else + tv.tv_usec = (HYBBX_TELNET_POLL_MS % 1000) * 1000; +#endif + + pr = select(maxfd + 1, &rfds, NULL, NULL, &tv); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (FD_ISSET(STDIN_FILENO, &rfds)) { + int have_line = 0; + int eof_seen = 0; + + if (!editor_ready || + hybbx_client_line_editor_poll(&editor, inbuf, sizeof(inbuf), + &have_line, &eof_seen) != HYBBX_OK) { + running = 0; + } else if (eof_seen) { + running = 0; + } else if (have_line) { + if (send_line(fd, inbuf) != HYBBX_OK) { + break; + } + } + } + + if (FD_ISSET(fd, &rfds)) { + uint8_t buf[512]; + ssize_t n = recv(fd, buf, sizeof(buf), 0); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + if (cfg->verbose) { + fprintf(stderr, "hybbx-telnet: recv %zd bytes\n", n); + } + + if (hybbx_telnet_parser_feed(&parser, fd, buf, (size_t)n, + on_telnet_data, &display) != HYBBX_OK) { + break; + } + } +#else + struct pollfd pfds[2]; + int pr; + + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + pfds[1].fd = fd; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + + pr = poll(pfds, 2, HYBBX_TELNET_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if ((pfds[0].revents & POLLIN) != 0) { + int have_line = 0; + int eof_seen = 0; + + if (!editor_ready || + hybbx_client_line_editor_poll(&editor, inbuf, sizeof(inbuf), + &have_line, &eof_seen) != HYBBX_OK) { + running = 0; + } else if (eof_seen) { + running = 0; + } else if (have_line) { + if (send_line(fd, inbuf) != HYBBX_OK) { + break; + } + } + } + + if ((pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + + if ((pfds[1].revents & POLLIN) != 0) { + uint8_t buf[512]; + ssize_t n = recv(fd, buf, sizeof(buf), 0); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + if (cfg->verbose) { + fprintf(stderr, "hybbx-telnet: recv %zd bytes\n", n); + } + + if (hybbx_telnet_parser_feed(&parser, fd, buf, (size_t)n, + on_telnet_data, &display) != HYBBX_OK) { + break; + } + } +#endif + } + + if (editor_ready) { + hybbx_client_line_editor_stop(&editor); + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + hybbx_telnet_client_config_t cfg; + int fd = -1; + int opt; + int argi = 1; + const char *env; + + static const struct option long_opts[] = { + {"host", required_argument, NULL, 'H'}, + {"port", required_argument, NULL, 'p'}, + {"user", required_argument, NULL, 'u'}, + {"password", required_argument, NULL, 'P'}, + {"baud", required_argument, NULL, 1000}, + {"line-width", required_argument, NULL, 1001}, + {"pace", required_argument, NULL, 1002}, + {"ansi", required_argument, NULL, 1003}, + {"ipv6", no_argument, NULL, '6'}, + {"timeout", required_argument, NULL, 't'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + config_defaults(&cfg); + + env = getenv("HYBBX_HOST"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.host, env, sizeof(cfg.host)); + } + env = getenv("HYBBX_PORT"); + if (env != NULL && env[0] != '\0') { + cfg.port = (unsigned)strtoul(env, NULL, 10); + } + + while ((opt = getopt_long(argc, argv, "H:p:u:P:6t:vh", + long_opts, NULL)) != -1) { + switch (opt) { + case 'H': + hybbx_strlcpy(cfg.host, optarg, sizeof(cfg.host)); + break; + case 'p': + cfg.port = (unsigned)strtoul(optarg, NULL, 10); + break; + case 'u': + hybbx_strlcpy(cfg.login_user, optarg, sizeof(cfg.login_user)); + break; + case 'P': + hybbx_strlcpy(cfg.login_password, optarg, sizeof(cfg.login_password)); + break; + case '6': + cfg.ipv6 = 1; + break; + case 't': + cfg.timeout_sec = (unsigned)strtoul(optarg, NULL, 10); + break; + case 'v': + cfg.verbose = 1; + break; + case 'h': + print_usage(argv[0]); + return EXIT_SUCCESS; + case 1000: + cfg.baud = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1001: + cfg.line_width = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1002: + cfg.pace_output = hybbx_parse_bool(optarg, cfg.pace_output); + break; + case 1003: + cfg.ansi = hybbx_parse_bool(optarg, cfg.ansi); + break; + default: + print_usage(argv[0]); + return EXIT_FAILURE; + } + } + + argi = optind; + if (argi < argc) { + hybbx_strlcpy(cfg.host, argv[argi++], sizeof(cfg.host)); + } + if (argi < argc) { + cfg.port = (unsigned)strtoul(argv[argi++], NULL, 10); + } + + (void)cfg.timeout_sec; + + if (tcp_connect(&cfg, &fd) != 0) { + return EXIT_FAILURE; + } + + if (cfg.verbose) { + fprintf(stderr, "hybbx-telnet: connected to %s:%u (baud=%u width=%u)\n", + cfg.host, cfg.port, cfg.baud, cfg.line_width); + } + + run_session(fd, &cfg); + close(fd); + return EXIT_SUCCESS; +} diff --git a/src/clients/hybbx_terminal.c b/src/clients/hybbx_terminal.c new file mode 100644 index 0000000..b6e09e1 --- /dev/null +++ b/src/clients/hybbx_terminal.c @@ -0,0 +1,433 @@ +/** + * hybbx-terminal — HyBBX AX.25 / HBX circuit terminal client (CLI only). + * + * Connects to the internal HBX circuit hub, authenticates with link password, + * and exchanges TERMINAL and AX.25 UI frames for operator sessions. + */ + +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "client_line.h" +#include "client_display.h" +#include "hybbx/ax25.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/limits.h" +#include "hybbx/link.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_TERMINAL_POLL_MS 50 + +typedef struct hybbx_terminal_config { + char host[256]; + unsigned port; + char link_id[HYBBX_LINK_ID_MAX]; + char link_password[128]; + char link_role[HYBBX_LINK_ROLE_MAX]; + char mycall[32]; + char dest[32]; + char via[128]; + int ax25_ui; + int verbose; + int ipv6; +} hybbx_terminal_config_t; + +typedef struct terminal_session { + int fd; + const hybbx_terminal_config_t *cfg; + hybbx_circuit_decoder_t dec; + hybbx_client_display_t display; + hybbx_ax25_path_t path; + int path_ready; +} terminal_session_t; + +static void config_defaults(hybbx_terminal_config_t *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + hybbx_strlcpy(cfg->host, "127.0.0.1", sizeof(cfg->host)); + cfg->port = HYBBX_CIRCUIT_DEFAULT_PORT; + hybbx_strlcpy(cfg->link_id, "terminal-1", sizeof(cfg->link_id)); + hybbx_strlcpy(cfg->link_role, "gateway", sizeof(cfg->link_role)); +} + +static void print_usage(const char *prog) +{ + fprintf(stderr, + "hybbx-terminal — HyBBX AX.25 / HBX circuit terminal client (CLI only)\n" + "\n" + "Usage: %s [options] [host [port]]\n" + "\n" + "Options:\n" + " -H, --host HOST Circuit hub host (default 127.0.0.1)\n" + " -p, --port PORT Circuit hub port (default 7323)\n" + " --link-id ID Link identity for password auth\n" + " --link-password P Link password (HBX LINK_AUTH)\n" + " --link-role ROLE link/repeater edge role: gateway | repeater | link | digipeater\n" + " --mycall CALL AX.25 source (CALL or CALL-SSID)\n" + " --dest CALL AX.25 destination\n" + " --via LIST Comma-separated digipeater path\n" + " --ax25-ui Send lines as AX.25 UI frames (needs mycall/dest)\n" + " --baud N Pace output (default 2400, 0=off)\n" + " --line-width N Wrap display columns (default 80)\n" + " --pace yes|no Output pacing\n" + " --ansi yes|no Pass ANSI sequences\n" + " -6, --ipv6 Prefer IPv6 (reserved)\n" + " -v, --verbose Log HBX frames on stderr\n" + " -h, --help Show help\n" + "\n" + "Environment: HYBBX_CIRCUIT_HOST, HYBBX_CIRCUIT_PORT\n" + "\n", + prog); +} + +static int build_ax25_path(const hybbx_terminal_config_t *cfg, + hybbx_ax25_path_t *path) +{ + const char *cursor; + char token[32]; + + memset(path, 0, sizeof(*path)); + + if (cfg->dest[0] == '\0' || cfg->mycall[0] == '\0') { + return 0; + } + + if (hybbx_ax25_address_parse(cfg->dest, &path->dest) != HYBBX_OK) { + return 0; + } + if (hybbx_ax25_address_parse(cfg->mycall, &path->source) != HYBBX_OK) { + return 0; + } + + cursor = cfg->via; + while (cursor != NULL && *cursor != '\0' && path->digi_count < HYBBX_AX25_MAX_DIGI) { + const char *comma = strchr(cursor, ','); + size_t len; + + if (comma != NULL) { + len = (size_t)(comma - cursor); + } else { + len = strlen(cursor); + } + if (len >= sizeof(token)) { + len = sizeof(token) - 1; + } + memcpy(token, cursor, len); + token[len] = '\0'; + + if (hybbx_ax25_address_parse(token, &path->digi[path->digi_count]) == HYBBX_OK) { + path->digi_count++; + } + + cursor = comma != NULL ? comma + 1 : NULL; + } + + return 1; +} + +static void on_circuit_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + terminal_session_t *sess = (terminal_session_t *)userdata; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t ui_len; + + (void)flags; + + if (sess == NULL || len == 0) { + return; + } + + if (sess->cfg->verbose) { + fprintf(stderr, "hybbx-terminal: rx proto=%s (%u bytes)\n", + hybbx_circuit_proto_name(proto), (unsigned)len); + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_TERMINAL: + hybbx_client_display_write(&sess->display, payload, len); + break; + case HYBBX_CIRCUIT_PROTO_AX25_UI: + ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, ui, sizeof(ui)); + if (ui_len > 0) { + hybbx_client_display_write(&sess->display, ui, ui_len); + } + break; + case HYBBX_CIRCUIT_PROTO_AX25: + ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, sizeof(ui)); + if (ui_len > 0) { + hybbx_client_display_write(&sess->display, ui, ui_len); + } + break; + default: + break; + } +} + +static hybbx_result_t send_terminal_line(terminal_session_t *sess, + const char *line) +{ + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t len = strlen(line); + + frame_len = hybbx_circuit_encode_terminal(line, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(sess->fd, frame, frame_len); +} + +static hybbx_result_t send_ax25_ui_line(terminal_session_t *sess, + const char *line) +{ + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t len = strlen(line); + + if (!sess->path_ready) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_ax25_ui(&sess->path, + (const uint8_t *)line, len, + HYBBX_CIRCUIT_FLAG_TX, + frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(sess->fd, frame, frame_len); +} + +static int run_session(terminal_session_t *sess) +{ + hybbx_client_line_editor_t editor; + char inbuf[HYBBX_LINE_MAX + 2]; + int running = 1; + int editor_ready = 0; + + if (hybbx_client_line_editor_start(&editor, STDIN_FILENO) == 0) { + editor_ready = 1; + } + + while (running) { + struct pollfd pfds[2]; + int pr; + uint8_t buf[512]; + + pfds[0].fd = STDIN_FILENO; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + pfds[1].fd = sess->fd; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + + pr = poll(pfds, 2, HYBBX_TERMINAL_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if ((pfds[0].revents & POLLIN) != 0) { + int have_line = 0; + int eof_seen = 0; + + if (!editor_ready || + hybbx_client_line_editor_poll(&editor, inbuf, sizeof(inbuf), + &have_line, &eof_seen) != HYBBX_OK) { + running = 0; + } else if (eof_seen) { + running = 0; + } else if (have_line) { + hybbx_result_t rc; + + if (sess->cfg->ax25_ui && sess->path_ready) { + rc = send_ax25_ui_line(sess, inbuf); + } else { + rc = send_terminal_line(sess, inbuf); + } + if (rc != HYBBX_OK) { + break; + } + } + } + + if ((pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + + if ((pfds[1].revents & POLLIN) != 0) { + size_t read_len; + + if (hybbx_circuit_link_read(sess->fd, buf, sizeof(buf), + &read_len) != HYBBX_OK) { + break; + } + if (read_len == 0) { + continue; + } + + hybbx_circuit_decoder_feed(&sess->dec, buf, read_len, + on_circuit_frame, sess); + } + } + + if (editor_ready) { + hybbx_client_line_editor_stop(&editor); + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + hybbx_terminal_config_t cfg; + terminal_session_t sess; + const char *env; + int opt; + int argi; + hybbx_traffic_config_t traffic; + + static const struct option long_opts[] = { + {"host", required_argument, NULL, 'H'}, + {"port", required_argument, NULL, 'p'}, + {"link-id", required_argument, NULL, 1000}, + {"link-password", required_argument, NULL, 1001}, + {"link-role", required_argument, NULL, 1002}, + {"mycall", required_argument, NULL, 1003}, + {"dest", required_argument, NULL, 1004}, + {"via", required_argument, NULL, 1005}, + {"ax25-ui", no_argument, NULL, 1006}, + {"baud", required_argument, NULL, 1007}, + {"line-width", required_argument, NULL, 1008}, + {"pace", required_argument, NULL, 1009}, + {"ansi", required_argument, NULL, 1010}, + {"ipv6", no_argument, NULL, '6'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} + }; + + config_defaults(&cfg); + hybbx_traffic_config_defaults(&traffic); + + env = getenv("HYBBX_CIRCUIT_HOST"); + if (env != NULL && env[0] != '\0') { + hybbx_strlcpy(cfg.host, env, sizeof(cfg.host)); + } + env = getenv("HYBBX_CIRCUIT_PORT"); + if (env != NULL && env[0] != '\0') { + cfg.port = (unsigned)strtoul(env, NULL, 10); + } + + while ((opt = getopt_long(argc, argv, "H:p:6vh", long_opts, NULL)) != -1) { + switch (opt) { + case 'H': + hybbx_strlcpy(cfg.host, optarg, sizeof(cfg.host)); + break; + case 'p': + cfg.port = (unsigned)strtoul(optarg, NULL, 10); + break; + case '6': + cfg.ipv6 = 1; + break; + case 'v': + cfg.verbose = 1; + break; + case 'h': + print_usage(argv[0]); + return EXIT_SUCCESS; + case 1000: + hybbx_strlcpy(cfg.link_id, optarg, sizeof(cfg.link_id)); + break; + case 1001: + hybbx_strlcpy(cfg.link_password, optarg, sizeof(cfg.link_password)); + break; + case 1002: + hybbx_strlcpy(cfg.link_role, optarg, sizeof(cfg.link_role)); + break; + case 1003: + hybbx_strlcpy(cfg.mycall, optarg, sizeof(cfg.mycall)); + break; + case 1004: + hybbx_strlcpy(cfg.dest, optarg, sizeof(cfg.dest)); + break; + case 1005: + hybbx_strlcpy(cfg.via, optarg, sizeof(cfg.via)); + break; + case 1006: + cfg.ax25_ui = 1; + break; + case 1007: + traffic.baud = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1008: + traffic.line_width = (unsigned)strtoul(optarg, NULL, 10); + break; + case 1009: + traffic.pace_output = hybbx_parse_bool(optarg, traffic.pace_output); + break; + case 1010: + traffic.ansi = hybbx_parse_bool(optarg, traffic.ansi); + break; + default: + print_usage(argv[0]); + return EXIT_FAILURE; + } + } + + argi = optind; + if (argi < argc) { + hybbx_strlcpy(cfg.host, argv[argi++], sizeof(cfg.host)); + } + if (argi < argc) { + cfg.port = (unsigned)strtoul(argv[argi++], NULL, 10); + } + + memset(&sess, 0, sizeof(sess)); + sess.cfg = &cfg; + hybbx_circuit_decoder_init(&sess.dec); + hybbx_client_display_init(&sess.display, &traffic); + sess.path_ready = build_ax25_path(&cfg, &sess.path); + + if (hybbx_circuit_link_connect(cfg.host, cfg.port, &sess.fd) != HYBBX_OK) { + fprintf(stderr, "hybbx-terminal: connect %s:%u failed\n", + cfg.host, cfg.port); + return EXIT_FAILURE; + } + + if (cfg.link_password[0] != '\0') { + if (hybbx_circuit_link_authenticate(sess.fd, cfg.link_password, + cfg.link_role, cfg.link_id) != HYBBX_OK) { + fprintf(stderr, "hybbx-terminal: link authentication failed\n"); + close(sess.fd); + return EXIT_FAILURE; + } + } + + if (cfg.verbose) { + fprintf(stderr, "hybbx-terminal: connected to %s:%u (ax25-ui=%s)\n", + cfg.host, cfg.port, cfg.ax25_ui ? "yes" : "no"); + } + + run_session(&sess); + close(sess.fd); + return EXIT_SUCCESS; +} diff --git a/src/clients/link_auth.c b/src/clients/link_auth.c new file mode 100644 index 0000000..4ba53e5 --- /dev/null +++ b/src/clients/link_auth.c @@ -0,0 +1,182 @@ +#include "hybbx/link.h" +#include "hybbx/util.h" + +#include +#include +#include + +static const char *find_kv_line(const char *payload, size_t len, + const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = payload; + const char *end = payload + len; + size_t key_len = strlen(key); + + while (cursor < end) { + const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor)); + const char *line_stop = line_end != NULL ? line_end : end; + const char *eq = memchr(cursor, '=', (size_t)(line_stop - cursor)); + + if (eq != NULL && (size_t)(eq - cursor) == key_len && + memcmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(line_stop - value); + + while (value_len > 0 && + (value[value_len - 1] == '\r' || + value[value_len - 1] == '\n' || + value[value_len - 1] == ' ')) { + value_len--; + } + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (line_end == NULL) { + break; + } + cursor = line_end + 1; + } + + return NULL; +} + +void hybbx_link_auth_clear(hybbx_link_auth_t *auth) +{ + if (auth == NULL) { + return; + } + + memset(auth, 0, sizeof(*auth)); +} + +size_t hybbx_link_auth_format(const hybbx_link_auth_t *auth, + char *out, size_t out_cap) +{ + int n; + + if (auth == NULL || out == NULL || out_cap == 0) { + return 0; + } + + if (auth->password[0] == '\0' || auth->id[0] == '\0') { + return 0; + } + + n = snprintf(out, out_cap, + "password=%s\nrole=%s\nid=%s\n", + auth->password, + auth->role[0] != '\0' ? auth->role : "link", + auth->id); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + if (auth->baud > 0) { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "baud=%u\n", auth->baud); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + if (auth->duplex == 1) { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "duplex=half\n"); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } else if (auth->duplex == 2) { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "duplex=full\n"); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + if (auth->bandwidth[0] != '\0') { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "bandwidth=%s\n", auth->bandwidth); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + if (auth->frequency_mhz[0] != '\0') { + int extra = snprintf(out + (size_t)n, out_cap - (size_t)n, + "frequency_mhz=%s\n", auth->frequency_mhz); + if (extra < 0 || (size_t)n + (size_t)extra >= out_cap) { + return 0; + } + n += extra; + } + + return (size_t)n; +} + +hybbx_result_t hybbx_link_auth_parse(const char *payload, size_t len, + hybbx_link_auth_t *auth) +{ + char scratch[128]; + + if (payload == NULL || auth == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_link_auth_clear(auth); + + if (find_kv_line(payload, len, "password", scratch, sizeof(scratch)) == NULL) { + return HYBBX_ERR_INVALID; + } + hybbx_strlcpy(auth->password, scratch, sizeof(auth->password)); + + if (find_kv_line(payload, len, "id", scratch, sizeof(scratch)) == NULL) { + return HYBBX_ERR_INVALID; + } + hybbx_strlcpy(auth->id, scratch, sizeof(auth->id)); + + if (find_kv_line(payload, len, "role", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->role, scratch, sizeof(auth->role)); + } else { + hybbx_strlcpy(auth->role, "link", sizeof(auth->role)); + } + + if (find_kv_line(payload, len, "baud", scratch, sizeof(scratch)) != NULL) { + unsigned long baud = strtoul(scratch, NULL, 10); + if (baud > 0) { + auth->baud = (unsigned)baud; + } + } + + if (find_kv_line(payload, len, "duplex", scratch, sizeof(scratch)) != NULL) { + if (strcmp(scratch, "full") == 0 || strcmp(scratch, "full-duplex") == 0) { + auth->duplex = 2; + } else if (strcmp(scratch, "half") == 0 || + strcmp(scratch, "half-duplex") == 0 || + strcmp(scratch, "simplex") == 0) { + auth->duplex = 1; + } + } + + if (find_kv_line(payload, len, "bandwidth", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->bandwidth, scratch, sizeof(auth->bandwidth)); + } + + if (find_kv_line(payload, len, "frequency_mhz", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->frequency_mhz, scratch, sizeof(auth->frequency_mhz)); + } else if (find_kv_line(payload, len, "frequency", scratch, + sizeof(scratch)) != NULL) { + hybbx_strlcpy(auth->frequency_mhz, scratch, sizeof(auth->frequency_mhz)); + } + + return HYBBX_OK; +} diff --git a/src/core/auth.c b/src/core/auth.c new file mode 100644 index 0000000..e68242a --- /dev/null +++ b/src/core/auth.c @@ -0,0 +1,477 @@ +#include "hybbx/auth.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +void hybbx_auth_config_defaults(hybbx_auth_config_t *auth) +{ + if (auth == NULL) { + return; + } + + auth->auto_login = 1; + hybbx_strlcpy(auth->guest_prefix, HYBBX_AUTH_DEFAULT_GUEST_PREFIX, + sizeof(auth->guest_prefix)); +} + +const char *hybbx_user_level_name(hybbx_user_level_t level) +{ + switch (level) { + case HYBBX_LEVEL_SYSOP: + return "sysop"; + case HYBBX_LEVEL_ADMIN: + return "admin"; + case HYBBX_LEVEL_MOD: + return "mod"; + case HYBBX_LEVEL_USER: + return "user"; + case HYBBX_LEVEL_GUEST: + return "guest"; + default: + return "user"; + } +} + +hybbx_user_level_t hybbx_user_level_parse(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return HYBBX_LEVEL_USER; + } + + if (str_ieq(name, "sysop")) { + return HYBBX_LEVEL_SYSOP; + } + if (str_ieq(name, "admin")) { + return HYBBX_LEVEL_ADMIN; + } + if (str_ieq(name, "mod") || str_ieq(name, "moderator")) { + return HYBBX_LEVEL_MOD; + } + if (str_ieq(name, "guest")) { + return HYBBX_LEVEL_GUEST; + } + + return HYBBX_LEVEL_USER; +} + +int hybbx_user_level_is_guest(hybbx_user_level_t level) +{ + return level == HYBBX_LEVEL_GUEST; +} + +int hybbx_user_level_is_sysop_or_admin(hybbx_user_level_t level) +{ + return level == HYBBX_LEVEL_SYSOP || level == HYBBX_LEVEL_ADMIN; +} + +int hybbx_user_level_is_sysop(hybbx_user_level_t level) +{ + return level == HYBBX_LEVEL_SYSOP; +} + +static int name_has_prefix(const char *username, const char *prefix) +{ + size_t plen; + size_t i; + + if (username == NULL || prefix == NULL) { + return 0; + } + + plen = strlen(prefix); + if (plen == 0) { + return 0; + } + + for (i = 0; i < plen; i++) { + char cu = username[i]; + char cp = prefix[i]; + + if (cu == '\0') { + return 0; + } + + if (cu >= 'A' && cu <= 'Z') { + cu = (char)(cu + 32); + } + if (cp >= 'A' && cp <= 'Z') { + cp = (char)(cp + 32); + } + + if (cu != cp) { + return 0; + } + } + + return 1; +} + +static int username_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '_' || ch == '-'; +} + +void hybbx_username_normalize(char *username) +{ + size_t i; + + if (username == NULL) { + return; + } + + for (i = 0; username[i] != '\0'; i++) { + if (username[i] >= 'A' && username[i] <= 'Z') { + username[i] = (char)(username[i] + 32); + } + } +} + +const char *hybbx_username_display(const char *username, + hybbx_user_level_t level) +{ + hybbx_user_record_t user; + + if (username == NULL || username[0] == '\0') { + return ""; + } + + memset(&user, 0, sizeof(user)); + hybbx_strlcpy(user.username, username, sizeof(user.username)); + hybbx_username_normalize(user.username); + user.level = level; + hybbx_nickname_infer(username, user.nickname, sizeof(user.nickname)); + + return hybbx_user_display_name(&user); +} + +void hybbx_nickname_infer(const char *stored_username, + char *nickname, + size_t nickname_len) +{ + if (stored_username == NULL || nickname == NULL || nickname_len == 0) { + return; + } + + if (str_ieq(stored_username, HYBBX_DEFAULT_SYSOP_USERNAME) || + str_ieq(stored_username, "sysop")) { + hybbx_strlcpy(nickname, HYBBX_DEFAULT_SYSOP_USERNAME, nickname_len); + return; + } + + if (stored_username[0] >= 'a' && stored_username[0] <= 'z') { + nickname[0] = (char)(stored_username[0] - 32); + hybbx_strlcpy(nickname + 1, stored_username + 1, nickname_len - 1); + return; + } + + hybbx_strlcpy(nickname, stored_username, nickname_len); +} + +const char *hybbx_user_display_name(const hybbx_user_record_t *user) +{ + if (user == NULL) { + return ""; + } + + if (user->nickname[0] != '\0') { + return user->nickname; + } + + if (user->username[0] == '\0') { + return ""; + } + + return user->username; +} + +int hybbx_guest_slot_from_username(const char *guest_prefix, + const char *username, + unsigned *slot_out) +{ + const char *prefix; + size_t plen; + size_t i; + unsigned long slot; + char *end; + + if (username == NULL || slot_out == NULL) { + return 0; + } + + prefix = guest_prefix != NULL && guest_prefix[0] != '\0' ? + guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + plen = strlen(prefix); + if (plen == 0 || strlen(username) <= plen) { + return 0; + } + + for (i = 0; i < plen; i++) { + char a = (char)(prefix[i] >= 'A' && prefix[i] <= 'Z' ? + prefix[i] + 32 : prefix[i]); + char b = (char)(username[i] >= 'A' && username[i] <= 'Z' ? + username[i] + 32 : username[i]); + + if (a != b) { + return 0; + } + } + + slot = strtoul(username + plen, &end, 10); + if (end == username + plen || *end != '\0' || slot < 1 || + slot > HYBBX_GUEST_NUMBER_MAX) { + return 0; + } + + *slot_out = (unsigned)slot; + return 1; +} + +void hybbx_guest_fill_record(const char *guest_prefix, + unsigned slot, + hybbx_user_record_t *out) +{ + const char *prefix; + time_t now; + + if (out == NULL || slot < 1 || slot > HYBBX_GUEST_NUMBER_MAX) { + return; + } + + prefix = guest_prefix != NULL && guest_prefix[0] != '\0' ? + guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + now = time(NULL); + + memset(out, 0, sizeof(*out)); + out->id = HYBBX_GUEST_USER_ID(slot); + snprintf(out->username, sizeof(out->username), "%s%u", prefix, slot); + hybbx_strlcpy(out->nickname, out->username, sizeof(out->nickname)); + out->level = HYBBX_LEVEL_GUEST; + out->active = 1; + out->created_at = now; +} + +int hybbx_username_valid(const char *username, const char *guest_prefix) +{ + size_t len; + size_t i; + size_t digit_count = 0; + size_t underscore_count = 0; + size_t hyphen_count = 0; + const char *prefix; + + if (username == NULL) { + return 0; + } + + len = strlen(username); + if (len < HYBBX_USERNAME_MIN_LEN || len > HYBBX_USERNAME_MAX_LEN) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!username_char_ok(username[i])) { + return 0; + } + + if (username[i] >= '0' && username[i] <= '9') { + digit_count++; + } else if (username[i] == '_') { + underscore_count++; + } else if (username[i] == '-') { + hyphen_count++; + } + } + + if (digit_count > HYBBX_USERNAME_MAX_DIGITS) { + return 0; + } + + if (underscore_count > 1 || hyphen_count > 1 || + (underscore_count > 0 && hyphen_count > 0)) { + return 0; + } + + prefix = guest_prefix != NULL && guest_prefix[0] != '\0' ? + guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + + if (name_has_prefix(username, prefix)) { + return 0; + } + + if (str_ieq(username, "sysop") || str_ieq(username, "admin") || + str_ieq(username, "mod") || str_ieq(username, "guest")) { + return 0; + } + + return 1; +} + +int hybbx_password_plain_valid(const char *password) +{ + size_t len; + + if (password == NULL || password[0] == '\0') { + return 0; + } + + if (str_ieq(password, "-")) { + return 0; + } + + len = strlen(password); + if (len < HYBBX_PASSWORD_MIN_LEN || len > HYBBX_PASSWORD_MAX_LEN) { + return 0; + } + + return 1; +} + +static int profile_text_char_ok(char ch) +{ + if (ch == '|') { + return 0; + } + + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '\'' || ch == '.' || ch == ','; +} + +static int profile_text_valid(const char *text, size_t min_len, size_t max_len) +{ + size_t len; + size_t i; + + if (text == NULL) { + return 0; + } + + len = strlen(text); + if (len < min_len || len >= max_len) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!profile_text_char_ok(text[i])) { + return 0; + } + } + + return 1; +} + +static int email_valid(const char *email) +{ + const char *at; + const char *dot; + size_t len; + size_t i; + + if (email == NULL) { + return 0; + } + + len = strlen(email); + if (len < 5 || len >= HYBBX_USER_EMAIL_MAX) { + return 0; + } + + at = strchr(email, '@'); + if (at == NULL || at == email || at[1] == '\0') { + return 0; + } + + dot = strchr(at + 1, '.'); + if (dot == NULL || dot[1] == '\0') { + return 0; + } + + for (i = 0; i < len; i++) { + char ch = email[i]; + + if (ch == '|' || ch == ' ') { + return 0; + } + + if (!((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '@' || ch == '.' || ch == '-' || ch == '_' || ch == '+')) { + return 0; + } + } + + return 1; +} + +int hybbx_user_profile_valid(const hybbx_user_registration_t *reg) +{ + if (reg == NULL) { + return 0; + } + + if (!profile_text_valid(reg->full_name, 2, HYBBX_USER_FULL_NAME_MAX)) { + return 0; + } + + if (!profile_text_valid(reg->country, 2, HYBBX_USER_COUNTRY_MAX)) { + return 0; + } + + if (!profile_text_valid(reg->location, 2, HYBBX_USER_LOCATION_MAX)) { + return 0; + } + + if (!email_valid(reg->email)) { + return 0; + } + + return 1; +} + +int hybbx_registration_valid(const hybbx_user_registration_t *reg, + const char *guest_prefix) +{ + if (reg == NULL) { + return 0; + } + + if (!hybbx_username_valid(reg->username, guest_prefix)) { + return 0; + } + + if (!hybbx_username_valid(reg->nickname, guest_prefix)) { + return 0; + } + + return hybbx_user_profile_valid(reg); +} diff --git a/src/core/bandwidth_policy.c b/src/core/bandwidth_policy.c new file mode 100644 index 0000000..47a59d7 --- /dev/null +++ b/src/core/bandwidth_policy.c @@ -0,0 +1,229 @@ +#include "hybbx/bandwidth_policy.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/plugin.h" +#include "hybbx/log.h" + +#include +#include +#include + +#define BW_USER_MAX 128u + +/** Lower = sacrificed first under pressure (AX.25 users before full-duplex TCP). */ +#define BW_PRIORITY_AX25_USER 0 +#define BW_PRIORITY_TCP_USER 1 + +typedef struct bw_user_entry { + hybbx_session_t *session; + time_t connected_at; + int sacrifice_priority; +} bw_user_entry_t; + +typedef struct bw_collect_ctx { + bw_user_entry_t entries[BW_USER_MAX]; + unsigned count; +} bw_collect_ctx_t; + +static int session_sacrifice_priority(const hybbx_session_t *session) +{ + const hybbx_transport_plugin_t *transport; + + if (session == NULL) { + return -1; + } + + transport = session->transport; + if (transport == NULL) { + return -1; + } + + switch (transport->kind) { + case HYBBX_TRANSPORT_PACKET_RADIO: + return BW_PRIORITY_AX25_USER; + case HYBBX_TRANSPORT_TELNET: + return BW_PRIORITY_TCP_USER; + case HYBBX_TRANSPORT_CIRCUIT: + default: + return -1; + } +} + +static void bw_collect_visitor(hybbx_session_t *session, void *userdata) +{ + bw_collect_ctx_t *ctx = (bw_collect_ctx_t *)userdata; + int priority; + + if (ctx == NULL || session == NULL) { + return; + } + + priority = session_sacrifice_priority(session); + if (priority < 0 || !hybbx_session_logged_in(session)) { + return; + } + + if (ctx->count >= BW_USER_MAX) { + return; + } + + ctx->entries[ctx->count].session = session; + ctx->entries[ctx->count].connected_at = hybbx_session_connected_at(session); + ctx->entries[ctx->count].sacrifice_priority = priority; + ctx->count++; +} + +static void bw_sort_victims_first(bw_collect_ctx_t *ctx) +{ + unsigned i; + unsigned j; + + if (ctx == NULL) { + return; + } + + /* Lowest sacrifice_priority first (AX.25), then newest connected_at. */ + for (i = 0; i + 1 < ctx->count; i++) { + for (j = i + 1; j < ctx->count; j++) { + int swap = 0; + + if (ctx->entries[j].sacrifice_priority < + ctx->entries[i].sacrifice_priority) { + swap = 1; + } else if (ctx->entries[j].sacrifice_priority == + ctx->entries[i].sacrifice_priority) { + if (ctx->entries[j].connected_at > + ctx->entries[i].connected_at) { + swap = 1; + } else if (ctx->entries[j].connected_at == + ctx->entries[i].connected_at && + hybbx_session_id(ctx->entries[j].session) > + hybbx_session_id(ctx->entries[i].session)) { + swap = 1; + } + } + + if (swap) { + bw_user_entry_t tmp = ctx->entries[i]; + + ctx->entries[i] = ctx->entries[j]; + ctx->entries[j] = tmp; + } + } + } +} + +static hybbx_session_t *bw_first_unpaused(const bw_collect_ctx_t *ctx) +{ + unsigned i; + + if (ctx == NULL) { + return NULL; + } + + for (i = 0; i < ctx->count; i++) { + if (!hybbx_session_bandwidth_paused(ctx->entries[i].session)) { + return ctx->entries[i].session; + } + } + + return NULL; +} + +static hybbx_session_t *bw_first_paused(const bw_collect_ctx_t *ctx) +{ + unsigned i; + + if (ctx == NULL) { + return NULL; + } + + for (i = 0; i < ctx->count; i++) { + if (hybbx_session_bandwidth_paused(ctx->entries[i].session)) { + return ctx->entries[i].session; + } + } + + return NULL; +} + +unsigned hybbx_bandwidth_policy_user_count(hybbx_service_t *service) +{ + bw_collect_ctx_t ctx; + + if (service == NULL) { + return 0; + } + + memset(&ctx, 0, sizeof(ctx)); + hybbx_service_visit_sessions(service, bw_collect_visitor, &ctx); + return ctx.count; +} + +unsigned hybbx_bandwidth_policy_apply(hybbx_service_t *service, + hybbx_circuit_balance_action_t action) +{ + bw_collect_ctx_t ctx; + unsigned i; + unsigned affected = 0; + + if (service == NULL) { + return 0; + } + + memset(&ctx, 0, sizeof(ctx)); + hybbx_service_visit_sessions(service, bw_collect_visitor, &ctx); + if (ctx.count == 0) { + return 0; + } + + bw_sort_victims_first(&ctx); + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: { + hybbx_session_t *target = bw_first_unpaused(&ctx); + + if (target != NULL) { + hybbx_session_set_bandwidth_paused(target, 1); + hybbx_log_stats("[bandwidth] paused user %s (QoS: AX.25 before TCP)", + hybbx_session_display_name(target)); + affected = 1; + } + break; + } + case HYBBX_CIRCUIT_BAL_BREAK: { + hybbx_session_t *target = bw_first_paused(&ctx); + + if (target == NULL && ctx.count > 0) { + target = ctx.entries[0].session; + } + + if (target != NULL) { + hybbx_log_stats("[bandwidth] disconnecting user %s (break)", + hybbx_session_display_name(target)); + hybbx_session_disconnect_bandwidth(target); + affected = 1; + } + break; + } + case HYBBX_CIRCUIT_BAL_CANCEL: + for (i = 0; i + 1 < ctx.count; i++) { + hybbx_log_stats("[bandwidth] disconnecting user %s (cancel)", + hybbx_session_display_name(ctx.entries[i].session)); + hybbx_session_disconnect_bandwidth(ctx.entries[i].session); + affected++; + } + break; + case HYBBX_CIRCUIT_BAL_RESUME: + for (i = 0; i < ctx.count; i++) { + hybbx_session_set_bandwidth_paused(ctx.entries[i].session, 0); + } + affected = ctx.count; + break; + case HYBBX_CIRCUIT_BAL_NONE: + default: + break; + } + + return affected; +} diff --git a/src/core/broadcast.c b/src/core/broadcast.c new file mode 100644 index 0000000..6a7cf68 --- /dev/null +++ b/src/core/broadcast.c @@ -0,0 +1,982 @@ +#include "hybbx/broadcast.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/plugin.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit.h" +#include "hybbx/ax25.h" +#include "hybbx/texts.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include + +void hybbx_ax25_frequency_table_clear(hybbx_ax25_frequency_table_t *table) +{ + if (table == NULL) { + return; + } + + memset(table, 0, sizeof(*table)); +} + +int hybbx_ax25_frequency_match(double a_mhz, double b_mhz) +{ + double diff; + + if (a_mhz <= 0.0 || b_mhz <= 0.0) { + return 0; + } + + diff = a_mhz - b_mhz; + if (diff < 0.0) { + diff = -diff; + } + + return diff < 0.001; +} + +void hybbx_ax25_frequency_apply(hybbx_ax25_frequency_table_t *table, + const hybbx_config_t *config) +{ + unsigned count; + unsigned i; + unsigned loaded = 0; + char key[24]; + const char *value; + + if (table == NULL || config == NULL) { + return; + } + + hybbx_ax25_frequency_table_clear(table); + + count = hybbx_config_get_uint(config, "ax25", "frequency_count", + 0u, 0u, HYBBX_AX25_FREQUENCY_MAX); + + for (i = 1; i <= HYBBX_AX25_FREQUENCY_MAX; i++) { + snprintf(key, sizeof(key), "frequency%u", i); + value = hybbx_config_get(config, "ax25", key, NULL); + if (value == NULL || value[0] == '\0') { + if (count > 0 && i > count) { + break; + } + continue; + } + + { + double mhz = strtod(value, NULL); + + if (mhz <= 0.0) { + continue; + } + if (loaded >= HYBBX_AX25_FREQUENCY_MAX) { + break; + } + + table->mhz[loaded] = mhz; + + snprintf(key, sizeof(key), "frequency%u_label", i); + value = hybbx_config_get(config, "ax25", key, NULL); + if (value == NULL || value[0] == '\0') { + snprintf(key, sizeof(key), "frequency%u_name", i); + value = hybbx_config_get(config, "ax25", key, NULL); + } + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(table->labels[loaded], value, + sizeof(table->labels[loaded])); + } else { + snprintf(table->labels[loaded], sizeof(table->labels[loaded]), + "%.3f MHz", mhz); + } + loaded++; + } + + if (count > 0 && i >= count) { + break; + } + } + + table->count = loaded; + + if (table->count > 0) { + hybbx_log_info("[ax25] %u configured frequencies (%.3f … %.3f MHz)", + table->count, table->mhz[0], table->mhz[table->count - 1]); + } else { + hybbx_log_info("[ax25] no frequencies in INI (set frequency1 … in [ax25])"); + } +} + +void hybbx_broadcast_config_defaults(hybbx_broadcast_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->ax25_enabled = 1; + cfg->ax25_auto = 1; + cfg->ax25_auto_interval_sec = HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC; + cfg->ax25_auto_stagger_sec = 0; + hybbx_strlcpy(cfg->ax25_mycall, "HYBBX", sizeof(cfg->ax25_mycall)); + hybbx_strlcpy(cfg->ax25_dest, "QST", sizeof(cfg->ax25_dest)); + hybbx_strlcpy(cfg->ax25_auto_message, HYBBX_BROADCAST_AUTO_MESSAGE_DEFAULT, + sizeof(cfg->ax25_auto_message)); + hybbx_ax25_frequency_table_clear(&cfg->frequencies); +} + +void hybbx_broadcast_config_apply(hybbx_broadcast_config_t *cfg, + const hybbx_config_t *config) +{ + const char *mycall; + const char *dest; + const char *auto_msg; + unsigned interval; + + if (cfg == NULL || config == NULL) { + return; + } + + hybbx_broadcast_config_defaults(cfg); + + cfg->enabled = hybbx_config_get_bool(config, "broadcast", "enabled", 1); + cfg->ax25_enabled = hybbx_config_get_bool(config, "broadcast", "ax25", 1); + cfg->ax25_auto = hybbx_config_get_bool(config, "broadcast", "ax25_auto", 1); + + interval = hybbx_config_get_uint(config, "broadcast", "ax25_auto_interval", + HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + 86400u); + cfg->ax25_auto_interval_sec = interval; + cfg->ax25_auto_stagger_sec = 0; + + mycall = hybbx_config_get(config, "broadcast", "ax25_mycall", NULL); + dest = hybbx_config_get(config, "broadcast", "ax25_dest", NULL); + auto_msg = hybbx_config_get(config, "broadcast", "ax25_auto_message", NULL); + if (mycall != NULL && mycall[0] != '\0') { + hybbx_ax25_address_t addr; + + if (hybbx_ax25_address_parse(mycall, &addr) == HYBBX_OK) { + hybbx_strlcpy(cfg->ax25_mycall, mycall, sizeof(cfg->ax25_mycall)); + } else { + hybbx_log_warn("[broadcast] ax25_mycall=%s invalid — using HYBBX", + mycall); + } + } + if (dest != NULL && dest[0] != '\0') { + hybbx_ax25_address_t addr; + + if (hybbx_ax25_address_parse(dest, &addr) == HYBBX_OK) { + hybbx_strlcpy(cfg->ax25_dest, dest, sizeof(cfg->ax25_dest)); + } else { + hybbx_log_warn("[broadcast] ax25_dest=%s invalid — using QST", dest); + hybbx_strlcpy(cfg->ax25_dest, "QST", sizeof(cfg->ax25_dest)); + } + } + if (auto_msg != NULL && auto_msg[0] != '\0') { + size_t msg_len = strlen(auto_msg); + + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + hybbx_log_warn("[broadcast] ax25_auto_message truncated (%zu > %u)", + msg_len, (unsigned)HYBBX_BROADCAST_AX25_MESSAGE_MAX); + } + hybbx_strlcpy(cfg->ax25_auto_message, auto_msg, + sizeof(cfg->ax25_auto_message)); + } + + hybbx_ax25_frequency_apply(&cfg->frequencies, config); + + hybbx_log_info("[broadcast] announce=%s ax25_auto=%s interval=%us " + "(min %us, band idle %us, link gap %us, per-link min %us)", + cfg->enabled ? "yes" : "no", + cfg->ax25_auto ? "yes" : "no", + cfg->ax25_auto_interval_sec, + (unsigned)HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC, + (unsigned)HYBBX_BROADCAST_AX25_BAND_IDLE_SEC, + (unsigned)HYBBX_BROADCAST_AX25_LINK_GAP_SEC, + (unsigned)HYBBX_BROADCAST_AX25_LINK_MIN_SEC); +} + +static hybbx_result_t broadcast_build_path(const hybbx_broadcast_config_t *cfg, + hybbx_ax25_path_t *path) +{ + if (cfg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(path, 0, sizeof(*path)); + + if (cfg->ax25_dest[0] == '\0' || strcmp(cfg->ax25_dest, "*") == 0) { + hybbx_log_warn("[broadcast] ax25_dest=%s invalid — using QST", + cfg->ax25_dest[0] != '\0' ? cfg->ax25_dest : "(empty)"); + if (hybbx_ax25_address_parse("QST", &path->dest) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + } else if (hybbx_ax25_address_parse(cfg->ax25_dest, &path->dest) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + if (hybbx_ax25_address_parse(cfg->ax25_mycall, &path->source) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static time_t g_ax25_last_sent; +static time_t g_ax25_link_last_sent[HYBBX_CIRCUIT_MAX_LINKS]; +static unsigned g_ax25_auto_tick; +static unsigned g_ax25_defer_log_sec; + +typedef struct broadcast_ax25_seq { + int active; + hybbx_service_t *service; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned next_index; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + time_t resume_at; +} broadcast_ax25_seq_t; + +static broadcast_ax25_seq_t g_ax25_seq; + +static double broadcast_link_target_mhz(const hybbx_broadcast_config_t *cfg, + const hybbx_circuit_broadcast_link_t *link, + unsigned link_index) +{ + (void)cfg; + (void)link_index; + + if (link == NULL) { + return 0.0; + } + + return link->frequency_mhz; +} + +static void broadcast_ax25_log_deferred(double frequency_mhz, const char *reason) +{ + if (reason == NULL) { + return; + } + + g_ax25_defer_log_sec++; + if (g_ax25_defer_log_sec < 60u) { + return; + } + + g_ax25_defer_log_sec = 0; + if (frequency_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 %.3f MHz deferred (%s)", + frequency_mhz, reason); + } else { + hybbx_log_stats("[broadcast] ax25 auto deferred (%s)", reason); + } +} + +static void broadcast_ax25_seq_defer_until(time_t resume_at, double target_mhz, + const char *reason) +{ + time_t now = time(NULL); + + broadcast_ax25_log_deferred(target_mhz, reason); + if (now != (time_t)-1 && (resume_at == (time_t)-1 || resume_at <= now)) { + resume_at = now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC; + } + g_ax25_seq.resume_at = resume_at; +} + +static int broadcast_hub_link_band_idle(hybbx_circuit_hub_t *hub, + unsigned slot_index) +{ + if (hub == NULL) { + return 0; + } + + return hybbx_circuit_hub_link_band_idle(hub, slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); +} + +static int broadcast_ax25_rate_ok(unsigned interval_sec) +{ + time_t now; + time_t elapsed; + + if (g_ax25_last_sent == 0) { + return 1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return 0; + } + + elapsed = now - g_ax25_last_sent; + if (elapsed < 0) { + return 1; + } + + return (unsigned)elapsed >= interval_sec; +} + +static int broadcast_ax25_link_rate_ok(unsigned slot_index, + unsigned interval_sec) +{ + time_t now; + time_t elapsed; + + if (slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return 0; + } + + if (g_ax25_link_last_sent[slot_index] == 0) { + return 1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return 0; + } + + elapsed = now - g_ax25_link_last_sent[slot_index]; + if (elapsed < 0) { + return 1; + } + + return (unsigned)elapsed >= interval_sec; +} + +static void broadcast_ax25_mark_sent(void) +{ + time_t now = time(NULL); + + if (now != (time_t)-1) { + g_ax25_last_sent = now; + } +} + +static void broadcast_ax25_mark_link_sent(unsigned slot_index) +{ + time_t now = time(NULL); + + if (slot_index >= HYBBX_CIRCUIT_MAX_LINKS || now == (time_t)-1) { + return; + } + + g_ax25_link_last_sent[slot_index] = now; + g_ax25_last_sent = now; +} + +static size_t broadcast_expand_service_token(char *out, size_t out_len, + const char *tmpl, + const char *service_name) +{ + size_t pos = 0; + size_t i = 0; + + if (out == NULL || out_len == 0 || tmpl == NULL) { + return 0; + } + + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + + out[0] = '\0'; + + while (tmpl[i] != '\0' && pos + 1 < out_len) { + if (strncmp(tmpl + i, HYBBX_BANNER_TOKEN_SERVICE, + strlen(HYBBX_BANNER_TOKEN_SERVICE)) == 0) { + size_t n = strlen(service_name); + + if (pos + n >= out_len) { + n = out_len - pos - 1; + } + memcpy(out + pos, service_name, n); + pos += n; + i += strlen(HYBBX_BANNER_TOKEN_SERVICE); + continue; + } + + out[pos++] = tmpl[i++]; + } + + out[pos] = '\0'; + return pos; +} + +static hybbx_result_t broadcast_ax25_send_slot(hybbx_service_t *service, + unsigned slot_index, + const char *message, + int enforce_rate_limit) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_ax25_path_t path; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t msg_len; + hybbx_result_t rc; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + const char *link_id = ""; + double target_mhz = 0.0; + + if (service == NULL || message == NULL || message[0] == '\0' || + slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].slot_index == slot_index) { + link_id = links[i].link_id; + target_mhz = links[i].frequency_mhz; + break; + } + } + if (link_id[0] == '\0') { + return HYBBX_ERR_DENIED; + } + + if (!hybbx_circuit_hub_link_band_idle(hub, slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC)) { + return HYBBX_ERR_BUSY; + } + + if (enforce_rate_limit && + !broadcast_ax25_link_rate_ok(slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + return HYBBX_ERR_BUSY; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_circuit_hub_link_broadcast_qos(hub)) { + return HYBBX_ERR_BUSY; + } + + rc = broadcast_build_path(cfg, &path); + if (rc != HYBBX_OK) { + return rc; + } + + { + uint8_t ax25_frame[HYBBX_AX25_FRAME_MAX]; + size_t ax25_len = hybbx_ax25_build_ui(&path, + (const uint8_t *)message, msg_len, + ax25_frame, sizeof(ax25_frame)); + + if (ax25_len == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[broadcast] ax25 path %s>%s payload=%zu slot=%u", + path.source.call, path.dest.call, msg_len, slot_index); + + frame_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + HYBBX_CIRCUIT_FLAG_TX, + ax25_frame, ax25_len, + frame, sizeof(frame)); + } + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + rc = hybbx_circuit_hub_send_hbx_slot(hub, slot_index, frame, frame_len, 1); + if (rc != HYBBX_OK) { + return rc; + } + + broadcast_ax25_mark_link_sent(slot_index); + + if (target_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 link=%s %.3f MHz (slot %u): %s", + link_id, target_mhz, slot_index, message); + } else { + hybbx_log_stats("[broadcast] ax25 link=%s (slot %u): %s", + link_id, slot_index, message); + } + + return HYBBX_OK; +} + +static hybbx_result_t broadcast_ax25_send(hybbx_service_t *service, + double frequency_mhz, + const char *message, + int enforce_rate_limit) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_ax25_path_t path; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + size_t msg_len; + hybbx_result_t rc; + unsigned sent_links; + int per_link = 0; + + if (service == NULL || message == NULL || message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + if (frequency_mhz > 0.0) { + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + int any_rate_ok = 0; + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].frequency_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, + links[i].frequency_mhz)) { + continue; + } + if (!enforce_rate_limit || + broadcast_ax25_link_rate_ok(links[i].slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + any_rate_ok = 1; + break; + } + } + if (!any_rate_ok) { + return HYBBX_ERR_BUSY; + } + per_link = 1; + } else if (enforce_rate_limit && + !broadcast_ax25_rate_ok(cfg->ax25_auto_interval_sec)) { + return HYBBX_ERR_BUSY; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_AX25_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_circuit_hub_link_broadcast_qos(hub)) { + return HYBBX_ERR_BUSY; + } + + rc = broadcast_build_path(cfg, &path); + if (rc != HYBBX_OK) { + return rc; + } + + { + uint8_t ax25_frame[HYBBX_AX25_FRAME_MAX]; + size_t ax25_len = hybbx_ax25_build_ui(&path, + (const uint8_t *)message, msg_len, + ax25_frame, sizeof(ax25_frame)); + + if (ax25_len == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[broadcast] ax25 path %s>%s payload=%zu", + path.source.call, path.dest.call, msg_len); + + frame_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + HYBBX_CIRCUIT_FLAG_TX, + ax25_frame, ax25_len, + frame, sizeof(frame)); + } + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + rc = hybbx_circuit_hub_multicast_hbx(hub, frame, frame_len, + frequency_mhz, 1, &sent_links); + if (rc != HYBBX_OK) { + return rc; + } + + if (per_link && frequency_mhz > 0.0) { + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned i; + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + for (i = 0; i < link_count; i++) { + if (links[i].frequency_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, + links[i].frequency_mhz)) { + continue; + } + broadcast_ax25_mark_link_sent(links[i].slot_index); + } + } else { + broadcast_ax25_mark_sent(); + } + + if (frequency_mhz > 0.0) { + hybbx_log_stats("[broadcast] ax25 %.3f MHz (%u hub): %s", + frequency_mhz, sent_links, message); + } else { + hybbx_log_stats("[broadcast] ax25 all qualifying links (%u hub): %s", + sent_links, message); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_broadcast_ax25(hybbx_service_t *service, + double frequency_mhz, + const char *message) +{ + return broadcast_ax25_send(service, frequency_mhz, message, 1); +} + +static int broadcast_ax25_seq_start(hybbx_service_t *service, + hybbx_circuit_hub_t *hub, + const hybbx_circuit_broadcast_link_t *links, + unsigned link_count, + const char *message) +{ + if (g_ax25_seq.active || service == NULL || hub == NULL || + links == NULL || link_count == 0 || message == NULL || + message[0] == '\0' || link_count > HYBBX_CIRCUIT_MAX_LINKS) { + return 0; + } + + memset(&g_ax25_seq, 0, sizeof(g_ax25_seq)); + g_ax25_seq.active = 1; + g_ax25_seq.service = service; + g_ax25_seq.hub = hub; + memcpy(g_ax25_seq.links, links, + link_count * sizeof(g_ax25_seq.links[0])); + g_ax25_seq.link_count = link_count; + hybbx_strlcpy(g_ax25_seq.message, message, sizeof(g_ax25_seq.message)); + return 1; +} + +static void broadcast_ax25_seq_advance(void) +{ + const hybbx_broadcast_config_t *cfg; + time_t now; + unsigned idx; + double target_mhz; + hybbx_result_t rc; + int advance = 0; + + if (!g_ax25_seq.active) { + return; + } + + if (g_ax25_seq.hub == NULL || + !hybbx_circuit_hub_running(g_ax25_seq.hub)) { + g_ax25_seq.active = 0; + return; + } + + now = time(NULL); + if (now == (time_t)-1) { + return; + } + + if (g_ax25_seq.resume_at != 0 && now < g_ax25_seq.resume_at) { + return; + } + + if (g_ax25_seq.next_index >= g_ax25_seq.link_count) { + g_ax25_seq.active = 0; + return; + } + + cfg = hybbx_service_get_broadcast(g_ax25_seq.service); + if (cfg == NULL) { + g_ax25_seq.active = 0; + return; + } + + idx = g_ax25_seq.next_index; + target_mhz = broadcast_link_target_mhz(cfg, &g_ax25_seq.links[idx], idx); + + if (target_mhz <= 0.0) { + hybbx_log_stats("[broadcast] ax25 skip link=%s (no MHz)", + g_ax25_seq.links[idx].link_id); + advance = 1; + } else if (!broadcast_hub_link_band_idle(g_ax25_seq.hub, + g_ax25_seq.links[idx].slot_index)) { + time_t ready = hybbx_circuit_hub_link_band_ready_at( + g_ax25_seq.hub, g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); + + broadcast_ax25_seq_defer_until(ready, target_mhz, "band busy"); + return; + } else if (!broadcast_ax25_link_rate_ok(g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_LINK_MIN_SEC)) { + broadcast_ax25_seq_defer_until( + now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC, + target_mhz, "link rate"); + return; + } else { + rc = broadcast_ax25_send_slot(g_ax25_seq.service, + g_ax25_seq.links[idx].slot_index, + g_ax25_seq.message, 0); + if (rc == HYBBX_OK) { + g_ax25_defer_log_sec = 0; + advance = 1; + } else if (rc == HYBBX_ERR_BUSY) { + if (!hybbx_circuit_hub_running(g_ax25_seq.hub)) { + g_ax25_seq.active = 0; + return; + } + { + time_t ready = hybbx_circuit_hub_link_band_ready_at( + g_ax25_seq.hub, g_ax25_seq.links[idx].slot_index, + HYBBX_BROADCAST_AX25_BAND_IDLE_SEC); + + broadcast_ax25_seq_defer_until(ready, target_mhz, + "band busy"); + } + return; + } else if (rc == HYBBX_ERR_DENIED) { + broadcast_ax25_seq_defer_until( + now + (time_t)HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC, + target_mhz, "no qualifying link"); + return; + } else { + hybbx_log_warn("[broadcast] ax25 %.3f MHz failed (%d)", + target_mhz, (int)rc); + advance = 1; + } + } + + if (!advance) { + return; + } + + g_ax25_seq.next_index++; + if (g_ax25_seq.next_index < g_ax25_seq.link_count) { + g_ax25_seq.resume_at = now + (time_t)HYBBX_BROADCAST_AX25_LINK_GAP_SEC; + } else { + g_ax25_seq.active = 0; + g_ax25_seq.resume_at = 0; + } +} + +void hybbx_broadcast_ax25_seq_cancel(void) +{ + g_ax25_seq.active = 0; + g_ax25_seq.resume_at = 0; +} + +hybbx_result_t hybbx_broadcast_ax25_manual(hybbx_service_t *service) +{ + const hybbx_broadcast_config_t *cfg; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + unsigned link_count; + + if (service == NULL) { + return HYBBX_ERR_INVALID; + } + + if (g_ax25_seq.active) { + return HYBBX_ERR_BUSY; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return HYBBX_ERR_BUSY; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + if (link_count == 0) { + return HYBBX_ERR_DENIED; + } + + broadcast_expand_service_token(message, sizeof(message), + cfg->ax25_auto_message, + hybbx_service_get_name(service)); + if (message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_log_info("[broadcast] ax25 manual sequential (%u link(s)): %s", + link_count, message); + + if (!broadcast_ax25_seq_start(service, hub, links, link_count, message)) { + return HYBBX_ERR_BUSY; + } + + broadcast_ax25_seq_advance(); + return HYBBX_OK; +} + +void hybbx_broadcast_ax25_tick(hybbx_service_t *service) +{ + const hybbx_broadcast_config_t *cfg; + char message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1]; + hybbx_circuit_hub_t *hub; + hybbx_circuit_broadcast_link_t links[HYBBX_CIRCUIT_MAX_LINKS]; + unsigned link_count; + unsigned interval; + + if (service == NULL) { + return; + } + + if (g_ax25_seq.active) { + broadcast_ax25_seq_advance(); + return; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled || !cfg->ax25_enabled || !cfg->ax25_auto) { + return; + } + + hub = hybbx_service_circuit_hub(service); + if (hub == NULL || !hybbx_circuit_hub_running(hub)) { + return; + } + + interval = cfg->ax25_auto_interval_sec; + if (interval == 0) { + return; + } + + link_count = hybbx_circuit_hub_broadcast_links(hub, links, + HYBBX_CIRCUIT_MAX_LINKS); + if (link_count == 0) { + return; + } + + g_ax25_auto_tick++; + + broadcast_expand_service_token(message, sizeof(message), + cfg->ax25_auto_message, + hybbx_service_get_name(service)); + if (message[0] == '\0') { + return; + } + + if (g_ax25_auto_tick < interval) { + return; + } + if (!broadcast_ax25_rate_ok(interval)) { + return; + } + + hybbx_log_info("[broadcast] ax25 auto sequential (%u link(s)): %s", + link_count, message); + + if (!broadcast_ax25_seq_start(service, hub, links, link_count, message)) { + return; + } + + g_ax25_auto_tick = 0; + broadcast_ax25_seq_advance(); +} + +typedef struct broadcast_announce_ctx { + hybbx_session_t *from; + const char *from_name; + const char *message; +} broadcast_announce_ctx_t; + +static void broadcast_announce_visitor(hybbx_session_t *session, void *userdata) +{ + broadcast_announce_ctx_t *ctx = (broadcast_announce_ctx_t *)userdata; + char line[HYBBX_LINE_MAX]; + + if (session == NULL || ctx == NULL || ctx->message == NULL) { + return; + } + + /* + * Local /broadcast is for online human users only. Circuit link adapter + * sessions (HBX bridges to TNCs) must not receive terminal fan-out — + * it floods low-bandwidth circuit queues and triggers load-balance + * pause/break on packet-radio links. + */ + if (!hybbx_session_is_interactive_user(session)) { + return; + } + if (!hybbx_session_logged_in(session)) { + return; + } + + if (hybbx_msg_format_sysop(line, sizeof(line), + ctx->from_name != NULL ? ctx->from_name : "Announce", + ctx->message) != HYBBX_OK) { + return; + } + if (ctx->from == NULL || session != ctx->from) { + (void)hybbx_session_command_gap(session); + } + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_broadcast_announce(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + broadcast_announce_ctx_t ctx; + const hybbx_broadcast_config_t *cfg; + size_t msg_len; + + if (service == NULL || message == NULL || message[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + cfg = hybbx_service_get_broadcast(service); + if (cfg == NULL || !cfg->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + msg_len = strlen(message); + if (msg_len > HYBBX_BROADCAST_MESSAGE_MAX) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.from_name = (from != NULL) ? hybbx_session_display_name(from) : "Announce"; + ctx.message = message; + hybbx_service_visit_sessions(service, broadcast_announce_visitor, &ctx); + + hybbx_log_stats("[broadcast] announce to local Main (%zu bytes): %s", + msg_len, message); + return HYBBX_OK; +} diff --git a/src/core/chat.c b/src/core/chat.c new file mode 100644 index 0000000..fdd4fa4 --- /dev/null +++ b/src/core/chat.c @@ -0,0 +1,463 @@ +#include "hybbx/chat.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/traffic.h" +#include "hybbx/log.h" + +#include +#include +#include +#include + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int channel_name_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '_'; +} + +static int channel_name_valid(const char *name) +{ + size_t len; + size_t i; + + if (name == NULL) { + return 0; + } + + len = strlen(name); + if (len < 2 || len >= HYBBX_CHAT_CHANNEL_NAME_MAX) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!channel_name_char_ok(name[i])) { + return 0; + } + } + + return 1; +} + +void hybbx_chat_config_defaults(hybbx_chat_config_t *chat) +{ + unsigned i; + + if (chat == NULL) { + return; + } + + chat->channel_count = HYBBX_DEFAULT_CHAT_CHANNELS; + chat->message_max = HYBBX_CHAT_MESSAGE_MAX; + + for (i = 0; i < HYBBX_CHAT_CHANNEL_MAX; i++) { + snprintf(chat->names[i], sizeof(chat->names[i]), "Channel%u", i + 1); + } +} + +void hybbx_chat_config_apply(hybbx_chat_config_t *chat, + const hybbx_config_t *config) +{ + unsigned count; + unsigned i; + char key[16]; + const char *value; + + if (chat == NULL || config == NULL) { + return; + } + + hybbx_chat_config_defaults(chat); + + count = hybbx_config_get_uint(config, "chat", "channels", + HYBBX_DEFAULT_CHAT_CHANNELS, 1u, + HYBBX_CHAT_CHANNEL_MAX); + chat->channel_count = count; + + chat->message_max = hybbx_config_get_uint(config, "chat", "message_max", + HYBBX_CHAT_MESSAGE_MAX, 1u, + HYBBX_LINE_MAX - 1u); + + for (i = 1; i <= chat->channel_count; i++) { + snprintf(key, sizeof(key), "channel%u", i); + value = hybbx_config_get(config, "chat", key, NULL); + if (value != NULL && value[0] != '\0' && channel_name_valid(value)) { + hybbx_strlcpy(chat->names[i - 1], value, + sizeof(chat->names[i - 1])); + } + } + + hybbx_log_info("[chat] channels=%u message_max=%u", + chat->channel_count, chat->message_max); +} + +const char *hybbx_chat_channel_name(const hybbx_chat_config_t *chat, + unsigned channel_index) +{ + if (chat == NULL || channel_index == 0 || + channel_index > chat->channel_count) { + return NULL; + } + + return chat->names[channel_index - 1]; +} + +hybbx_result_t hybbx_chat_resolve_channel(const hybbx_chat_config_t *chat, + const char *spec, + unsigned *out_index) +{ + char *end; + unsigned long parsed; + unsigned i; + + if (chat == NULL || spec == NULL || out_index == NULL || spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + parsed = strtoul(spec, &end, 10); + if (end != spec && *end == '\0' && parsed >= 1 && + parsed <= chat->channel_count) { + *out_index = (unsigned)parsed; + return HYBBX_OK; + } + + for (i = 1; i <= chat->channel_count; i++) { + if (str_ieq(chat->names[i - 1], spec)) { + *out_index = i; + return HYBBX_OK; + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +void hybbx_chat_list_channels(hybbx_session_t *session, + const hybbx_chat_config_t *chat) +{ + unsigned i; + char line[HYBBX_CHAT_CHANNEL_NAME_MAX + 32]; + unsigned current; + + if (session == NULL || chat == NULL) { + return; + } + + current = hybbx_session_chat_channel(session); + + hybbx_session_write_line(session, "Channels:"); + for (i = 1; i <= chat->channel_count; i++) { + snprintf(line, sizeof(line), " %u %s", i, chat->names[i - 1]); + hybbx_session_write_line(session, line); + } + + if (current > 0) { + snprintf(line, sizeof(line), "Current: %u %s", + current, hybbx_chat_channel_name(chat, current)); + hybbx_session_write_line(session, line); + } else { + hybbx_session_write_line(session, "Use /chat or ."); + } +} + +typedef struct chat_broadcast_ctx { + hybbx_session_t *from; + const char *message; + const char *from_user; + unsigned channel; +} chat_broadcast_ctx_t; + +static void chat_broadcast_visitor(hybbx_session_t *session, void *userdata) +{ + chat_broadcast_ctx_t *ctx = (chat_broadcast_ctx_t *)userdata; + char line[HYBBX_USER_NAME_MAX + HYBBX_LINE_MAX + 16]; + + if (session == NULL || ctx == NULL || ctx->message == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_chat_channel(session) != ctx->channel) { + return; + } + + if (session == ctx->from) { + snprintf(line, sizeof(line), "ME: %s", ctx->message); + } else { + snprintf(line, sizeof(line), "%s: %s", ctx->from_user, ctx->message); + } + + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_chat_post(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + chat_broadcast_ctx_t ctx; + const hybbx_chat_config_t *chat; + unsigned channel; + + if (service == NULL || from == NULL || message == NULL) { + return HYBBX_ERR_INVALID; + } + + if (message[0] == '\0') { + return HYBBX_OK; + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(message) > chat->message_max) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_session_area(from) != HYBBX_AREA_CHAT) { + return HYBBX_ERR_INVALID; + } + + channel = hybbx_session_chat_channel(from); + if (channel == 0) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.message = message; + ctx.from_user = hybbx_session_display_name(from); + ctx.channel = channel; + + hybbx_service_visit_sessions(service, chat_broadcast_visitor, &ctx); + return HYBBX_OK; +} + +#define CHAT_SHOWALL_MAX 128u + +typedef struct chat_show_entry { + unsigned channel; + char label[HYBBX_USER_NAME_MAX + HYBBX_CHAT_CHANNEL_NAME_MAX + 4]; +} chat_show_entry_t; + +typedef struct chat_show_ctx { + hybbx_session_t *requester; + unsigned channel; + unsigned count; + chat_show_entry_t entries[CHAT_SHOWALL_MAX]; + const hybbx_chat_config_t *chat; +} chat_show_ctx_t; + +static void chat_show_channel_visitor(hybbx_session_t *session, void *userdata) +{ + chat_show_ctx_t *ctx = (chat_show_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || session == ctx->requester) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_chat_channel(session) != ctx->channel) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + hybbx_session_write_line(ctx->requester, hybbx_session_display_name(session)); +} + +void hybbx_chat_show_channel(hybbx_service_t *service, hybbx_session_t *session) +{ + chat_show_ctx_t ctx; + unsigned channel; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + hybbx_session_write_line(session, "Not in a chat channel."); + return; + } + + channel = hybbx_session_chat_channel(session); + if (channel == 0) { + hybbx_session_write_line(session, "Not in a chat channel."); + return; + } + + ctx.requester = session; + ctx.channel = channel; + ctx.count = 0; + ctx.chat = NULL; + + hybbx_service_visit_sessions(service, chat_show_channel_visitor, &ctx); +} + +static void chat_show_all_collect(hybbx_session_t *session, void *userdata) +{ + chat_show_ctx_t *ctx = (chat_show_ctx_t *)userdata; + unsigned channel; + const char *ch_name; + chat_show_entry_t *entry; + + if (session == NULL || ctx == NULL || ctx->chat == NULL) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_CHAT) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + channel = hybbx_session_chat_channel(session); + if (channel == 0 || channel > ctx->chat->channel_count) { + return; + } + + if (ctx->count >= CHAT_SHOWALL_MAX) { + return; + } + + ch_name = hybbx_chat_channel_name(ctx->chat, channel); + if (ch_name == NULL) { + return; + } + + entry = &ctx->entries[ctx->count++]; + entry->channel = channel; + snprintf(entry->label, sizeof(entry->label), "%s@%s", + hybbx_session_display_name(session), ch_name); +} + +static int chat_show_entry_cmp(const void *a, const void *b) +{ + const chat_show_entry_t *ea = (const chat_show_entry_t *)a; + const chat_show_entry_t *eb = (const chat_show_entry_t *)b; + + if (ea->channel != eb->channel) { + return (ea->channel < eb->channel) ? -1 : 1; + } + + return strcmp(ea->label, eb->label); +} + +static void chat_showall_flush(hybbx_session_t *session, char *line, size_t *pos) +{ + if (session == NULL || line == NULL || pos == NULL || *pos == 0) { + return; + } + + line[*pos] = '\0'; + hybbx_session_write_line(session, line); + line[0] = '\0'; + *pos = 0; +} + +static void chat_showall_append(hybbx_session_t *session, char *line, + size_t line_size, size_t *pos, + const char *entry) +{ + size_t entry_len; + size_t need; + + if (session == NULL || line == NULL || pos == NULL || entry == NULL) { + return; + } + + entry_len = strlen(entry); + need = (*pos > 0) ? (*pos + 1 + entry_len) : entry_len; + + if (*pos > 0 && need > HYBBX_LINE_WIDTH) { + chat_showall_flush(session, line, pos); + need = entry_len; + } + + if (entry_len >= line_size) { + return; + } + + if (*pos > 0) { + line[(*pos)++] = ' '; + } + + memcpy(line + *pos, entry, entry_len); + *pos += entry_len; + line[*pos] = '\0'; +} + +void hybbx_chat_show_all(hybbx_service_t *service, hybbx_session_t *session) +{ + chat_show_ctx_t ctx; + char line[HYBBX_LINE_WIDTH + 1]; + size_t pos = 0; + unsigned i; + + if (service == NULL || session == NULL) { + return; + } + + ctx.requester = session; + ctx.channel = 0; + ctx.count = 0; + ctx.chat = hybbx_service_get_chat(service); + if (ctx.chat == NULL) { + return; + } + + hybbx_service_visit_sessions(service, chat_show_all_collect, &ctx); + + if (ctx.count == 0) { + return; + } + + qsort(ctx.entries, ctx.count, sizeof(ctx.entries[0]), chat_show_entry_cmp); + + line[0] = '\0'; + for (i = 0; i < ctx.count; i++) { + chat_showall_append(session, line, sizeof(line), &pos, ctx.entries[i].label); + } + + chat_showall_flush(session, line, &pos); +} diff --git a/src/core/circuit.c b/src/core/circuit.c new file mode 100644 index 0000000..04ac264 --- /dev/null +++ b/src/core/circuit.c @@ -0,0 +1,337 @@ +#include "hybbx/circuit.h" + +#include + +static uint32_t read_be32(const uint8_t *p) +{ + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | + ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} + +static uint16_t read_be16(const uint8_t *p) +{ + return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]); +} + +static void write_be32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)((v >> 24) & 0xFFu); + p[1] = (uint8_t)((v >> 16) & 0xFFu); + p[2] = (uint8_t)((v >> 8) & 0xFFu); + p[3] = (uint8_t)(v & 0xFFu); +} + +static void write_be16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)((v >> 8) & 0xFFu); + p[1] = (uint8_t)(v & 0xFFu); +} + +const char *hybbx_circuit_proto_name(hybbx_circuit_proto_t proto) +{ + switch (proto) { + case HYBBX_CIRCUIT_PROTO_AX25: + return "ax25"; + case HYBBX_CIRCUIT_PROTO_AX25_UI: + return "ax25_ui"; + case HYBBX_CIRCUIT_PROTO_LINK_AUTH: + return "link_auth"; + case HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK: + return "link_auth_ack"; + case HYBBX_CIRCUIT_PROTO_FLOW_CTRL: + return "flow_ctrl"; + case HYBBX_CIRCUIT_PROTO_TERMINAL: + return "terminal"; + case HYBBX_CIRCUIT_PROTO_PROXY_MAIL: + return "proxy_mail"; + case HYBBX_CIRCUIT_PROTO_PROXY_CHAT: + return "proxy_chat"; + case HYBBX_CIRCUIT_PROTO_RESERVED_APRS: + return "aprs"; + case HYBBX_CIRCUIT_PROTO_RESERVED_NETROM: + return "netrom"; + default: + return "unknown"; + } +} + +void hybbx_circuit_decoder_init(hybbx_circuit_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void circuit_deliver(hybbx_circuit_decoder_t *dec, + hybbx_circuit_frame_cb cb, void *userdata) +{ + const uint8_t *payload; + size_t payload_len; + + if (dec->len < HYBBX_CIRCUIT_HEADER_SIZE) { + dec->len = 0; + dec->need = 0; + dec->have_header = 0; + return; + } + + payload = dec->buf + HYBBX_CIRCUIT_HEADER_SIZE; + payload_len = dec->len - HYBBX_CIRCUIT_HEADER_SIZE; + + if (cb != NULL) { + cb(dec->proto, dec->flags, payload, payload_len, userdata); + } + + dec->len = 0; + dec->need = 0; + dec->have_header = 0; +} + +static int circuit_parse_header(hybbx_circuit_decoder_t *dec) +{ + const uint8_t *h = dec->buf; + + if (h[0] != HYBBX_CIRCUIT_MAGIC_0 || h[1] != HYBBX_CIRCUIT_MAGIC_1 || + h[2] != HYBBX_CIRCUIT_MAGIC_2 || h[3] != HYBBX_CIRCUIT_VERSION) { + return 0; + } + + dec->proto = (hybbx_circuit_proto_t)h[4]; + dec->flags = read_be16(h + 5); + dec->need = (size_t)read_be32(h + 7); + if (dec->need > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return 0; + } + + return 1; +} + +void hybbx_circuit_decoder_feed(hybbx_circuit_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_circuit_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + if (dec->len >= sizeof(dec->buf)) { + hybbx_circuit_decoder_init(dec); + } + + dec->buf[dec->len++] = data[i]; + + if (!dec->have_header) { + if (dec->len < HYBBX_CIRCUIT_HEADER_SIZE) { + continue; + } + if (!circuit_parse_header(dec)) { + memmove(dec->buf, dec->buf + 1, dec->len - 1); + dec->len--; + continue; + } + dec->have_header = 1; + } + + if (dec->len < HYBBX_CIRCUIT_HEADER_SIZE + dec->need) { + continue; + } + + circuit_deliver(dec, cb, userdata); + } +} + +size_t hybbx_circuit_encode(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + if (out == NULL || out_cap < HYBBX_CIRCUIT_HEADER_SIZE) { + return 0; + } + + if (payload_len > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return 0; + } + + if (payload_len > 0 && payload == NULL) { + return 0; + } + + if (HYBBX_CIRCUIT_HEADER_SIZE + payload_len > out_cap) { + return 0; + } + + out[0] = HYBBX_CIRCUIT_MAGIC_0; + out[1] = HYBBX_CIRCUIT_MAGIC_1; + out[2] = HYBBX_CIRCUIT_MAGIC_2; + out[3] = HYBBX_CIRCUIT_VERSION; + out[4] = (uint8_t)proto; + write_be16(out + 5, flags); + + write_be32(out + 7, (uint32_t)payload_len); + if (payload_len > 0) { + memcpy(out + HYBBX_CIRCUIT_HEADER_SIZE, payload, payload_len); + } + + return HYBBX_CIRCUIT_HEADER_SIZE + payload_len; +} + +size_t hybbx_circuit_encode_ax25(const uint8_t *frame, size_t frame_len, + uint16_t flags, + uint8_t *out, size_t out_cap) +{ + return hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25, + (uint16_t)(flags | HYBBX_CIRCUIT_FLAG_RX), + frame, frame_len, out, out_cap); +} + +static size_t pack_path_payload(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + unsigned i; + + if (path == NULL || out == NULL) { + return 0; + } + + if (1 + payload_len > out_cap) { + return 0; + } + + out[pos++] = (uint8_t)path->digi_count; + if (pos + HYBBX_AX25_ADDR_ENCODED * (2 + path->digi_count) > out_cap) { + return 0; + } + + hybbx_ax25_encode_address(out + pos, &path->dest, 0); + pos += HYBBX_AX25_ADDR_ENCODED; + for (i = 0; i < path->digi_count; i++) { + hybbx_ax25_encode_address(out + pos, &path->digi[i], 0); + pos += HYBBX_AX25_ADDR_ENCODED; + } + hybbx_ax25_encode_address(out + pos, &path->source, 1); + pos += HYBBX_AX25_ADDR_ENCODED; + + if (payload_len > 0) { + if (pos + payload_len > out_cap) { + return 0; + } + memcpy(out + pos, payload, payload_len); + pos += payload_len; + } + + return pos; +} + +size_t hybbx_circuit_encode_ax25_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint16_t flags, + uint8_t *out, size_t out_cap) +{ + uint8_t body[HYBBX_CIRCUIT_MAX_PAYLOAD]; + size_t body_len; + + body_len = pack_path_payload(path, payload, payload_len, body, sizeof(body)); + if (body_len == 0 && payload_len > 0) { + return 0; + } + + return hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_AX25_UI, + (uint16_t)(flags | HYBBX_CIRCUIT_FLAG_PATH), + body, body_len, out, out_cap); +} + +size_t hybbx_circuit_encode_terminal(const char *data, size_t len, + uint8_t *out, size_t out_cap) +{ + return hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_TX, + (const uint8_t *)data, len, out, out_cap); +} + +size_t hybbx_circuit_encode_link_msg(hybbx_circuit_proto_t proto, + const char *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + if (proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH && + proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK) { + return 0; + } + + return hybbx_circuit_encode(proto, HYBBX_CIRCUIT_FLAG_NONE, + (const uint8_t *)payload, payload_len, + out, out_cap); +} + +static void decode_address_field(const uint8_t *in, hybbx_ax25_address_t *out) +{ + size_t i; + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + char ch = (char)((in[i] >> 1) & 0x7Fu); + out->call[i] = ch == ' ' ? '\0' : ch; + } + out->call[HYBBX_AX25_CALL_MAX] = '\0'; + out->ssid = (unsigned)((in[6] >> 1) & 0x0Fu); +} + +size_t hybbx_circuit_unpack_ax25_ui(const uint8_t *payload, size_t payload_len, + hybbx_ax25_path_t *path, + uint8_t *ui, size_t ui_cap) +{ + size_t pos = 0; + unsigned digi_count; + unsigned i; + + if (payload == NULL || payload_len < 1) { + return 0; + } + + if (path != NULL) { + memset(path, 0, sizeof(*path)); + } + + digi_count = payload[0]; + if (digi_count > HYBBX_AX25_MAX_DIGI) { + return 0; + } + + pos = 1; + if (pos + HYBBX_AX25_ADDR_ENCODED * (size_t)(2 + digi_count) > payload_len) { + return 0; + } + + if (path != NULL) { + decode_address_field(payload + pos, &path->dest); + pos += HYBBX_AX25_ADDR_ENCODED; + for (i = 0; i < digi_count; i++) { + decode_address_field(payload + pos, &path->digi[i]); + pos += HYBBX_AX25_ADDR_ENCODED; + } + path->digi_count = digi_count; + decode_address_field(payload + pos, &path->source); + } else { + pos += HYBBX_AX25_ADDR_ENCODED * (size_t)(2 + digi_count); + } + pos += HYBBX_AX25_ADDR_ENCODED; + + { + size_t ui_len = payload_len - pos; + + if (ui_len > ui_cap) { + return 0; + } + + if (ui_len > 0 && ui != NULL) { + memcpy(ui, payload + pos, ui_len); + } + + return ui_len; + } +} diff --git a/src/core/circuit_balance.c b/src/core/circuit_balance.c new file mode 100644 index 0000000..5571e9d --- /dev/null +++ b/src/core/circuit_balance.c @@ -0,0 +1,545 @@ +#include "hybbx/circuit_balance.h" +#include "hybbx/util.h" + +#include +#include +#include + +typedef struct balance_frame { + uint8_t data[HYBBX_CIRCUIT_MAX_FRAME]; + size_t len; +} balance_frame_t; + +struct hybbx_circuit_balance { + hybbx_circuit_balance_config_t cfg; + hybbx_circuit_link_profile_t profile; + balance_frame_t queue[HYBBX_CIRCUIT_BALANCE_QUEUE_MAX]; + size_t q_head; + size_t q_count; + size_t queue_bytes; + hybbx_circuit_balance_action_t action; + hybbx_circuit_balance_action_t last_sent_action; + double drain_accum; + time_t oldest_enqueue; + int link_low_bw; +}; + +static int str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = *a; + char cb = *b; + + if (ca >= 'A' && ca <= 'Z') { + ca = (char)(ca - 'A' + 'a'); + } + if (cb >= 'A' && cb <= 'Z') { + cb = (char)(cb - 'A' + 'a'); + } + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv_line(const char *payload, size_t len, + const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = payload; + const char *end = payload + len; + size_t key_len = strlen(key); + + while (cursor < end) { + const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor)); + const char *line_stop = line_end != NULL ? line_end : end; + const char *eq = memchr(cursor, '=', (size_t)(line_stop - cursor)); + + if (eq != NULL && (size_t)(eq - cursor) == key_len && + memcmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(line_stop - value); + + while (value_len > 0 && + (value[value_len - 1] == '\r' || + value[value_len - 1] == '\n' || + value[value_len - 1] == ' ')) { + value_len--; + } + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (line_end == NULL) { + break; + } + cursor = line_end + 1; + } + + return NULL; +} + +void hybbx_circuit_balance_config_defaults(hybbx_circuit_balance_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->lag_sec = 5u; + cfg->queue_pause = 4096u; + cfg->queue_break = 16384u; + cfg->queue_cancel = 65536u; +} + +void hybbx_circuit_link_profile_from_auth(const hybbx_link_auth_t *auth, + hybbx_circuit_link_profile_t *out) +{ + unsigned baud; + + if (out == NULL) { + return; + } + + memset(out, 0, sizeof(*out)); + out->bandwidth = HYBBX_CIRCUIT_BW_HIGH; + out->duplex = HYBBX_CIRCUIT_DUPLEX_FULL; + out->baud = HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD; + + if (auth == NULL) { + return; + } + + baud = auth->baud; + out->duplex = (hybbx_circuit_duplex_t)auth->duplex; + + if (auth->bandwidth[0] != '\0') { + if (str_ieq_local(auth->bandwidth, "low")) { + out->bandwidth = HYBBX_CIRCUIT_BW_LOW; + } else if (str_ieq_local(auth->bandwidth, "high")) { + out->bandwidth = HYBBX_CIRCUIT_BW_HIGH; + } + } else if (baud > 0 && baud <= HYBBX_CIRCUIT_BALANCE_LOW_BAUD_THRESHOLD) { + out->bandwidth = HYBBX_CIRCUIT_BW_LOW; + } + + if (baud > 0) { + out->baud = baud; + } else if (out->bandwidth == HYBBX_CIRCUIT_BW_LOW) { + out->baud = 1200u; + } + + if (out->duplex == HYBBX_CIRCUIT_DUPLEX_UNSET) { + out->duplex = out->bandwidth == HYBBX_CIRCUIT_BW_LOW ? + HYBBX_CIRCUIT_DUPLEX_HALF : HYBBX_CIRCUIT_DUPLEX_FULL; + } + + if (auth->frequency_mhz[0] != '\0') { + out->frequency_mhz = strtod(auth->frequency_mhz, NULL); + } +} + +const char *hybbx_circuit_balance_action_name(hybbx_circuit_balance_action_t action) +{ + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + return "pause"; + case HYBBX_CIRCUIT_BAL_BREAK: + return "break"; + case HYBBX_CIRCUIT_BAL_CANCEL: + return "cancel"; + case HYBBX_CIRCUIT_BAL_RESUME: + return "resume"; + case HYBBX_CIRCUIT_BAL_NONE: + default: + return "none"; + } +} + +hybbx_result_t hybbx_circuit_flow_ctrl_parse(const char *payload, size_t len, + hybbx_circuit_balance_action_t *action_out, + char *reason_out, size_t reason_cap) +{ + char scratch[128]; + const char *action_str; + + if (payload == NULL || action_out == NULL) { + return HYBBX_ERR_INVALID; + } + + action_str = find_kv_line(payload, len, "action", scratch, sizeof(scratch)); + if (action_str == NULL) { + return HYBBX_ERR_INVALID; + } + + if (str_ieq_local(action_str, "pause")) { + *action_out = HYBBX_CIRCUIT_BAL_PAUSE; + } else if (str_ieq_local(action_str, "break")) { + *action_out = HYBBX_CIRCUIT_BAL_BREAK; + } else if (str_ieq_local(action_str, "cancel")) { + *action_out = HYBBX_CIRCUIT_BAL_CANCEL; + } else if (str_ieq_local(action_str, "resume")) { + *action_out = HYBBX_CIRCUIT_BAL_RESUME; + } else { + return HYBBX_ERR_INVALID; + } + + if (reason_out != NULL && reason_cap > 0) { + reason_out[0] = '\0'; + if (find_kv_line(payload, len, "reason", scratch, sizeof(scratch)) != NULL) { + hybbx_strlcpy(reason_out, scratch, reason_cap); + } + } + + return HYBBX_OK; +} + +size_t hybbx_circuit_flow_ctrl_format(hybbx_circuit_balance_action_t action, + const char *reason, + char *out, size_t out_cap) +{ + int n; + const char *reason_str = reason != NULL && reason[0] != '\0' ? reason : "-"; + + if (out == NULL || out_cap == 0) { + return 0; + } + + n = snprintf(out, out_cap, "action=%s\nreason=%s\n", + hybbx_circuit_balance_action_name(action), reason_str); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + return (size_t)n; +} + +hybbx_circuit_balance_t *hybbx_circuit_balance_create( + const hybbx_circuit_balance_config_t *cfg) +{ + hybbx_circuit_balance_t *bal; + + bal = calloc(1, sizeof(*bal)); + if (bal == NULL) { + return NULL; + } + + if (cfg != NULL) { + bal->cfg = *cfg; + } else { + hybbx_circuit_balance_config_defaults(&bal->cfg); + } + + hybbx_circuit_link_profile_from_auth(NULL, &bal->profile); + bal->link_low_bw = 0; + return bal; +} + +void hybbx_circuit_balance_destroy(hybbx_circuit_balance_t *bal) +{ + free(bal); +} + +void hybbx_circuit_balance_set_profile(hybbx_circuit_balance_t *bal, + const hybbx_circuit_link_profile_t *profile) +{ + if (bal == NULL || profile == NULL) { + return; + } + + bal->profile = *profile; + bal->link_low_bw = profile->bandwidth == HYBBX_CIRCUIT_BW_LOW; +} + +hybbx_circuit_balance_action_t hybbx_circuit_balance_action( + const hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return HYBBX_CIRCUIT_BAL_NONE; + } + + return bal->action; +} + +size_t hybbx_circuit_balance_queued_bytes(const hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return 0; + } + + return bal->queue_bytes; +} + +void hybbx_circuit_balance_spared_cancel(hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + bal->action = HYBBX_CIRCUIT_BAL_PAUSE; + bal->last_sent_action = HYBBX_CIRCUIT_BAL_NONE; + } +} + +static unsigned balance_effective_baud(const hybbx_circuit_balance_t *bal) +{ + unsigned baud; + + if (bal == NULL) { + return HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD; + } + + baud = bal->profile.baud; + if (baud == 0) { + baud = HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD; + } + return baud; +} + +static unsigned balance_estimated_lag_sec(const hybbx_circuit_balance_t *bal) +{ + unsigned baud; + unsigned lag; + + if (bal == NULL || bal->queue_bytes == 0) { + return 0; + } + + baud = balance_effective_baud(bal); + if (baud == 0) { + return 0; + } + + lag = (unsigned)((bal->queue_bytes * 8u) / baud); + if (bal->oldest_enqueue > 0) { + time_t now = time(NULL); + if (now > bal->oldest_enqueue) { + unsigned wall = (unsigned)(now - bal->oldest_enqueue); + if (wall > lag) { + lag = wall; + } + } + } + return lag; +} + +static void balance_send_flow(hybbx_circuit_balance_t *bal, + hybbx_circuit_balance_action_t action, + const char *reason, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx) +{ + if (bal == NULL || flow_fn == NULL) { + return; + } + if (action == bal->last_sent_action) { + return; + } + + flow_fn(flow_ctx, action, reason); + bal->last_sent_action = action; +} + +static void balance_clear_queue(hybbx_circuit_balance_t *bal) +{ + if (bal == NULL) { + return; + } + + bal->q_head = 0; + bal->q_count = 0; + bal->queue_bytes = 0; + bal->oldest_enqueue = 0; + bal->drain_accum = 0.0; +} + +static void balance_apply_escalation(hybbx_circuit_balance_t *bal, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx) +{ + unsigned lag; + const hybbx_circuit_balance_config_t *cfg; + + if (bal == NULL || !bal->cfg.enabled) { + return; + } + + cfg = &bal->cfg; + lag = balance_estimated_lag_sec(bal); + + if (bal->link_low_bw && + (bal->queue_bytes >= cfg->queue_cancel || + lag >= cfg->lag_sec * 2u)) { + if (bal->action != HYBBX_CIRCUIT_BAL_CANCEL) { + bal->action = HYBBX_CIRCUIT_BAL_CANCEL; + balance_clear_queue(bal); + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_CANCEL, "queue_overload", + flow_fn, flow_ctx); + } + return; + } + + if (bal->queue_bytes >= cfg->queue_break || lag >= cfg->lag_sec) { + if (bal->action != HYBBX_CIRCUIT_BAL_BREAK && + bal->action != HYBBX_CIRCUIT_BAL_CANCEL) { + bal->action = HYBBX_CIRCUIT_BAL_BREAK; + balance_clear_queue(bal); + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_BREAK, "queue_lag", + flow_fn, flow_ctx); + bal->action = HYBBX_CIRCUIT_BAL_PAUSE; + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_PAUSE, "stabilize", + flow_fn, flow_ctx); + } + return; + } + + if (bal->queue_bytes >= cfg->queue_pause || + lag >= (cfg->lag_sec > 0 ? cfg->lag_sec / 2u : 0u)) { + if (bal->action == HYBBX_CIRCUIT_BAL_NONE) { + bal->action = HYBBX_CIRCUIT_BAL_PAUSE; + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_PAUSE, "queue_pressure", + flow_fn, flow_ctx); + } + return; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_PAUSE && + bal->queue_bytes < cfg->queue_pause / 2u && + lag < (cfg->lag_sec > 0 ? cfg->lag_sec / 2u : 0u)) { + bal->action = HYBBX_CIRCUIT_BAL_NONE; + balance_send_flow(bal, HYBBX_CIRCUIT_BAL_RESUME, "stabilized", + flow_fn, flow_ctx); + } +} + +hybbx_result_t hybbx_circuit_balance_submit(hybbx_circuit_balance_t *bal, + const uint8_t *frame, size_t len, + hybbx_circuit_balance_send_fn send_fn, + void *send_ctx, + hybbx_circuit_balance_flow_fn flow_fn, + void *flow_ctx) +{ + size_t slot; + balance_frame_t *entry; + + if (bal == NULL || frame == NULL || len == 0 || send_fn == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!bal->cfg.enabled || + bal->profile.bandwidth == HYBBX_CIRCUIT_BW_HIGH) { + return send_fn(send_ctx, frame, len); + } + + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_ERR_BUSY; + } + + if (bal->q_count >= HYBBX_CIRCUIT_BALANCE_QUEUE_MAX) { + balance_apply_escalation(bal, flow_fn, flow_ctx); + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_ERR_BUSY; + } + return HYBBX_ERR_NOMEM; + } + + if (len > sizeof(entry->data)) { + return HYBBX_ERR_INVALID; + } + + slot = (bal->q_head + bal->q_count) % HYBBX_CIRCUIT_BALANCE_QUEUE_MAX; + entry = &bal->queue[slot]; + memcpy(entry->data, frame, len); + entry->len = len; + bal->q_count++; + bal->queue_bytes += len; + if (bal->oldest_enqueue == 0) { + bal->oldest_enqueue = time(NULL); + } + + balance_apply_escalation(bal, flow_fn, flow_ctx); + return HYBBX_OK; +} + +hybbx_circuit_balance_tick_result_t hybbx_circuit_balance_tick( + hybbx_circuit_balance_t *bal, unsigned poll_ms, + hybbx_circuit_balance_send_fn send_fn, void *send_ctx, + hybbx_circuit_balance_flow_fn flow_fn, void *flow_ctx) +{ + unsigned baud; + double budget; + size_t sent_budget; + + if (bal == NULL || send_fn == NULL) { + return HYBBX_CIRCUIT_BAL_TICK_OK; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK; + } + + if (!bal->cfg.enabled || bal->q_count == 0) { + balance_apply_escalation(bal, flow_fn, flow_ctx); + return HYBBX_CIRCUIT_BAL_TICK_OK; + } + + if (bal->action == HYBBX_CIRCUIT_BAL_PAUSE) { + balance_apply_escalation(bal, flow_fn, flow_ctx); + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK; + } + return HYBBX_CIRCUIT_BAL_TICK_OK; + } + + baud = balance_effective_baud(bal); + if (poll_ms == 0) { + poll_ms = 1; + } + + budget = ((double)baud * (double)poll_ms) / 8000.0; + bal->drain_accum += budget; + sent_budget = (size_t)bal->drain_accum; + bal->drain_accum -= (double)sent_budget; + + while (sent_budget > 0 && bal->q_count > 0) { + balance_frame_t *entry = &bal->queue[bal->q_head]; + + if (entry->len > sent_budget) { + break; + } + + if (send_fn(send_ctx, entry->data, entry->len) != HYBBX_OK) { + break; + } + + sent_budget -= entry->len; + bal->queue_bytes -= entry->len; + bal->q_head = (bal->q_head + 1u) % HYBBX_CIRCUIT_BALANCE_QUEUE_MAX; + bal->q_count--; + } + + if (bal->q_count == 0) { + bal->oldest_enqueue = 0; + } + + balance_apply_escalation(bal, flow_fn, flow_ctx); + if (bal->action == HYBBX_CIRCUIT_BAL_CANCEL) { + return HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK; + } + + return HYBBX_CIRCUIT_BAL_TICK_OK; +} diff --git a/src/core/circuit_bridge.c b/src/core/circuit_bridge.c new file mode 100644 index 0000000..af3bde4 --- /dev/null +++ b/src/core/circuit_bridge.c @@ -0,0 +1,175 @@ +#include "hybbx/circuit_bridge.h" +#include "hybbx/util.h" + +#include +#include +#include + +typedef struct bridge_load_ctx { + hybbx_circuit_bridge_registry_t *reg; + const hybbx_config_t *reg_config; +} bridge_load_ctx_t; + +static int bridge_section_is_numbered_transport(const char *section, + const char *plugin) +{ + char prefix[64]; + size_t prefix_len; + const char *suffix; + + if (section == NULL || plugin == NULL) { + return 0; + } + + snprintf(prefix, sizeof(prefix), "transport.%s", plugin); + prefix_len = strlen(prefix); + if (strncmp(section, prefix, prefix_len) != 0) { + return 0; + } + + suffix = section + prefix_len; + if (suffix[0] == '\0') { + return 0; + } + + while (*suffix != '\0') { + if (*suffix < '0' || *suffix > '9') { + return 0; + } + suffix++; + } + + return 1; +} + +static int bridge_section_is_numbered_packet_radio(const char *section) +{ + return bridge_section_is_numbered_transport(section, "packet_radio"); +} + +static int bridge_section_is_numbered_ardop(const char *section) +{ + return bridge_section_is_numbered_transport(section, "ardop"); +} + +static int bridge_section_is_numbered_crdop(const char *section) +{ + return bridge_section_is_numbered_transport(section, "crdop"); +} + +static int bridge_section_is_numbered_baycom(const char *section) +{ + return bridge_section_is_numbered_transport(section, "baycom"); +} + +static void bridge_load_section(const char *section, void *userdata) +{ + bridge_load_ctx_t *ctx = (bridge_load_ctx_t *)userdata; + hybbx_circuit_bridge_entry_t *entry; + const char *value; + unsigned i; + + if (ctx == NULL || ctx->reg == NULL || section == NULL) { + return; + } + + if (!bridge_section_is_numbered_packet_radio(section) && + !bridge_section_is_numbered_ardop(section) && + !bridge_section_is_numbered_crdop(section) && + !bridge_section_is_numbered_baycom(section) && + !bridge_section_is_numbered_transport(section, "mains_proxy")) { + return; + } + + if (ctx->reg->count >= HYBBX_CIRCUIT_MAX_LINKS) { + return; + } + + entry = &ctx->reg->entries[ctx->reg->count]; + memset(entry, 0, sizeof(*entry)); + + value = hybbx_config_get(ctx->reg_config, section, "link_id", NULL); + if (value == NULL || value[0] == '\0') { + return; + } + hybbx_strlcpy(entry->link_id, value, sizeof(entry->link_id)); + + for (i = 0; i < ctx->reg->count; i++) { + if (strcmp(ctx->reg->entries[i].link_id, entry->link_id) == 0) { + fprintf(stderr, + "[circuit] bridge: duplicate link_id=%s in [%s] — skipped\n", + entry->link_id, section); + return; + } + } + + value = hybbx_config_get(ctx->reg_config, section, "link_password", NULL); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(entry->link_password, value, sizeof(entry->link_password)); + } + + value = hybbx_config_get(ctx->reg_config, section, "link_role", NULL); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(entry->link_role, value, sizeof(entry->link_role)); + } else if (bridge_section_is_numbered_transport(section, "mains_proxy")) { + hybbx_strlcpy(entry->link_role, "proxy", sizeof(entry->link_role)); + } else { + hybbx_strlcpy(entry->link_role, "link", sizeof(entry->link_role)); + } + + value = hybbx_config_get(ctx->reg_config, section, "frequency_mhz", NULL); + if (value == NULL || value[0] == '\0') { + value = hybbx_config_get(ctx->reg_config, section, "frequency", NULL); + } + if (value != NULL && value[0] != '\0') { + double mhz = strtod(value, NULL); + + if (mhz > 0.0) { + entry->frequency_mhz = mhz; + } + } + + ctx->reg->count++; +} + +void hybbx_circuit_bridge_clear(hybbx_circuit_bridge_registry_t *reg) +{ + if (reg == NULL) { + return; + } + memset(reg, 0, sizeof(*reg)); +} + +hybbx_result_t hybbx_circuit_bridge_load(hybbx_circuit_bridge_registry_t *reg, + const hybbx_config_t *config) +{ + bridge_load_ctx_t ctx; + + if (reg == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_circuit_bridge_clear(reg); + ctx.reg = reg; + ctx.reg_config = config; + hybbx_config_foreach_section(config, bridge_load_section, &ctx); + return HYBBX_OK; +} + +const hybbx_circuit_bridge_entry_t *hybbx_circuit_bridge_find( + const hybbx_circuit_bridge_registry_t *reg, const char *link_id) +{ + unsigned i; + + if (reg == NULL || link_id == NULL || link_id[0] == '\0') { + return NULL; + } + + for (i = 0; i < reg->count; i++) { + if (strcmp(reg->entries[i].link_id, link_id) == 0) { + return ®->entries[i]; + } + } + + return NULL; +} diff --git a/src/core/circuit_tcp.c b/src/core/circuit_tcp.c new file mode 100644 index 0000000..58f10e4 --- /dev/null +++ b/src/core/circuit_tcp.c @@ -0,0 +1,1837 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/circuit_tcp.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/circuit.h" +#include "hybbx/broadcast.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/circuit_bridge.h" +#include "hybbx/bandwidth_policy.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/storage.h" +#include "hybbx/traffic.h" +#include "hybbx/link.h" +#include "hybbx/password.h" +#include "hybbx/socket.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_CIRCUIT_LINK_POLL_MS 50 +#define HYBBX_CIRCUIT_AUTH_TIMEOUT_MS 15000 + +typedef enum { + CIRCUIT_SLOT_FREE = 0, + CIRCUIT_SLOT_CONNECTING, + CIRCUIT_SLOT_ACTIVE +} circuit_slot_state_t; + +typedef struct hybbx_circuit_link_slot { + hybbx_circuit_hub_t *hub; + circuit_slot_state_t state; + int fd; + pthread_t thread; + char link_id[HYBBX_LINK_ID_MAX]; + hybbx_session_t *session; + hybbx_circuit_decoder_t decoder; + hybbx_circuit_balance_t *balance; + hybbx_circuit_link_profile_t profile; + int profile_set; + time_t last_rf_activity; +} hybbx_circuit_link_slot_t; + +struct hybbx_circuit_hub { + hybbx_service_t *service; + hybbx_circuit_config_t config; + hybbx_link_registry_t links; + hybbx_circuit_bridge_registry_t bridge; + unsigned max_links; + char link_password[128]; + int link_auth; + int listen_v4; + int listen_v6; + pthread_t accept_thread; + pthread_mutex_t lock; + volatile int running; + hybbx_circuit_link_slot_t slots[HYBBX_CIRCUIT_MAX_LINKS]; +}; + +void hybbx_circuit_config_defaults(hybbx_circuit_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + snprintf(cfg->bind4, sizeof(cfg->bind4), "127.0.0.1"); + snprintf(cfg->bind6, sizeof(cfg->bind6), "::1"); + cfg->port = HYBBX_CIRCUIT_DEFAULT_PORT; + cfg->ipv4 = 1; + cfg->ipv6 = 1; + cfg->link_stale_days = HYBBX_LINK_STALE_DAYS; + cfg->link_auth = 1; + hybbx_circuit_balance_config_defaults(&cfg->balance); + cfg->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + hybbx_circuit_bridge_clear(&cfg->bridge); +} + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port, + int backlog) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, backlog > 0 ? backlog : 8) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static int circuit_slot_broadcast_qos(const hybbx_circuit_link_slot_t *slot) +{ + if (slot == NULL || !slot->profile_set) { + return 0; + } + + return slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW && + slot->profile.duplex == HYBBX_CIRCUIT_DUPLEX_HALF; +} + +static int circuit_frame_is_ax25_broadcast_tx(const uint8_t *frame, size_t len) +{ + unsigned flags; + uint8_t proto; + + if (frame == NULL || len < HYBBX_CIRCUIT_HEADER_SIZE) { + return 0; + } + + if (frame[0] != HYBBX_CIRCUIT_MAGIC_0 || + frame[1] != HYBBX_CIRCUIT_MAGIC_1 || + frame[2] != HYBBX_CIRCUIT_MAGIC_2 || + frame[3] != HYBBX_CIRCUIT_VERSION) { + return 0; + } + + proto = frame[4]; + if (proto != (uint8_t)HYBBX_CIRCUIT_PROTO_AX25_UI && + proto != (uint8_t)HYBBX_CIRCUIT_PROTO_AX25) { + return 0; + } + + flags = ((unsigned)frame[5] << 8u) | (unsigned)frame[6]; + return (flags & HYBBX_CIRCUIT_FLAG_TX) != 0; +} + +static int circuit_slot_can_send_low_prio(const hybbx_circuit_hub_t *hub, + const hybbx_circuit_link_slot_t *slot) +{ + hybbx_circuit_balance_action_t action; + + if (hub == NULL || slot == NULL || slot->balance == NULL || + !hub->config.balance.enabled || !slot->profile_set) { + return 1; + } + + action = hybbx_circuit_balance_action(slot->balance); + if (action == HYBBX_CIRCUIT_BAL_CANCEL) { + return 0; + } + + /* + * Auto-beacon AX.25 uses circuit_slot_send_raw (no queue slot). Do not + * block on PAUSE/BREAK backlog — only CANCEL drops the link. + */ + return 1; +} + +static unsigned circuit_count_used_slots(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE) { + count++; + } + } + + return count; +} + +static int circuit_find_free_slot(hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return -1; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_FREE && hub->slots[i].fd < 0) { + return (int)i; + } + } + + return -1; +} + +static int circuit_find_active_slot_by_id(const hybbx_circuit_hub_t *hub, + const char *link_id, + const hybbx_circuit_link_slot_t *except) +{ + unsigned i; + + if (hub == NULL || link_id == NULL || link_id[0] == '\0') { + return -1; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + const hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot == except) { + continue; + } + if (slot->state == CIRCUIT_SLOT_FREE || slot->link_id[0] == '\0') { + continue; + } + if (strcmp(slot->link_id, link_id) == 0) { + return (int)i; + } + } + + return -1; +} + +typedef struct balance_send_ctx { + hybbx_circuit_link_slot_t *slot; +} balance_send_ctx_t; + +typedef struct flow_ctrl_ctx { + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; +} flow_ctrl_ctx_t; + +static hybbx_result_t circuit_slot_send_raw(hybbx_circuit_link_slot_t *slot, + const uint8_t *frame, size_t len) +{ + hybbx_circuit_hub_t *hub; + ssize_t sent; + size_t off = 0; + int fd; + + if (slot == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hub = slot->hub; + if (hub == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&hub->lock); + fd = slot->fd; + if (fd < 0) { + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_BUSY; + } + + while (off < len) { + sent = send(fd, frame + off, len - off, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_IO; + } + if (sent == 0) { + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_IO; + } + off += (size_t)sent; + } + + pthread_mutex_unlock(&hub->lock); + return HYBBX_OK; +} + +static hybbx_result_t balance_send_raw_cb(void *ctx, const uint8_t *frame, + size_t len) +{ + balance_send_ctx_t *bctx = (balance_send_ctx_t *)ctx; + + if (bctx == NULL || bctx->slot == NULL) { + return HYBBX_ERR_INVALID; + } + + return circuit_slot_send_raw(bctx->slot, frame, len); +} + +static int circuit_bandwidth_spare_link(hybbx_circuit_hub_t *hub, + hybbx_circuit_link_slot_t *slot, + hybbx_circuit_balance_action_t action) +{ + unsigned users_before; + unsigned affected; + + if (hub == NULL || hub->service == NULL || slot == NULL) { + return 0; + } + + users_before = hybbx_bandwidth_policy_user_count(hub->service); + if (users_before == 0) { + return 0; + } + + affected = hybbx_bandwidth_policy_apply(hub->service, action); + if (affected == 0) { + return 0; + } + + if (slot->balance != NULL && + hybbx_circuit_balance_action(slot->balance) == HYBBX_CIRCUIT_BAL_CANCEL) { + hybbx_circuit_balance_spared_cancel(slot->balance); + hybbx_log_stats("[circuit] secondary link %s spared — users sacrificed first", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + } + + return 1; +} + +static void balance_flow_ctrl_cb(void *ctx, + hybbx_circuit_balance_action_t action, + const char *reason) +{ + flow_ctrl_ctx_t *fctx = (flow_ctrl_ctx_t *)ctx; + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + char payload[128]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t payload_len; + size_t frame_len; + const char *reason_str = reason != NULL ? reason : "-"; + + if (fctx == NULL || fctx->hub == NULL || fctx->slot == NULL || + action == HYBBX_CIRCUIT_BAL_NONE) { + return; + } + + hub = fctx->hub; + slot = fctx->slot; + + payload_len = hybbx_circuit_flow_ctrl_format(action, reason_str, + payload, sizeof(payload)); + if (payload_len == 0) { + return; + } + + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_FLOW_CTRL, + payload, payload_len, + frame, sizeof(frame)); + if (frame_len > 0) { + (void)circuit_slot_send_raw(slot, frame, frame_len); + } + + if (action == HYBBX_CIRCUIT_BAL_CANCEL) { + if (circuit_bandwidth_spare_link(hub, slot, action)) { + return; + } + hybbx_log_warn("[circuit] load-balance cancelled link %s (%s)", + slot->link_id[0] != '\0' ? slot->link_id : "?", + reason_str); + } else if (action == HYBBX_CIRCUIT_BAL_PAUSE || + action == HYBBX_CIRCUIT_BAL_BREAK || + action == HYBBX_CIRCUIT_BAL_RESUME) { + hybbx_log_stats("[circuit] load-balance %s link=%s (%s)", + hybbx_circuit_balance_action_name(action), + slot->link_id[0] != '\0' ? slot->link_id : "?", + reason_str); + } + + if (hub->service != NULL && + (action == HYBBX_CIRCUIT_BAL_PAUSE || + action == HYBBX_CIRCUIT_BAL_BREAK || + action == HYBBX_CIRCUIT_BAL_RESUME)) { + (void)hybbx_bandwidth_policy_apply(hub->service, action); + } +} + +static void circuit_balance_tick_slot(hybbx_circuit_link_slot_t *slot, + int *cancel_link) +{ + balance_send_ctx_t bctx; + flow_ctrl_ctx_t fctx; + hybbx_circuit_balance_tick_result_t tr; + hybbx_circuit_hub_t *hub; + + if (cancel_link != NULL) { + *cancel_link = 0; + } + + if (slot == NULL || slot->hub == NULL || slot->balance == NULL || + !slot->profile_set) { + return; + } + + hub = slot->hub; + bctx.slot = slot; + fctx.hub = hub; + fctx.slot = slot; + + tr = hybbx_circuit_balance_tick(slot->balance, HYBBX_CIRCUIT_LINK_POLL_MS, + balance_send_raw_cb, &bctx, + balance_flow_ctrl_cb, &fctx); + if (slot->balance != NULL && slot->profile_set && + hub->config.balance.enabled && + slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW && + hybbx_circuit_balance_action(slot->balance) == HYBBX_CIRCUIT_BAL_PAUSE && + hybbx_circuit_balance_queued_bytes(slot->balance) >= + hub->config.balance.queue_pause) { + (void)circuit_bandwidth_spare_link(hub, slot, HYBBX_CIRCUIT_BAL_PAUSE); + } + if (tr == HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK && cancel_link != NULL) { + if (circuit_bandwidth_spare_link(hub, slot, HYBBX_CIRCUIT_BAL_CANCEL)) { + tr = HYBBX_CIRCUIT_BAL_TICK_OK; + } else { + *cancel_link = 1; + } + } +} + +static hybbx_result_t circuit_slot_send_hbx(hybbx_circuit_link_slot_t *slot, + const uint8_t *frame, size_t len) +{ + balance_send_ctx_t bctx; + flow_ctrl_ctx_t fctx; + hybbx_circuit_hub_t *hub; + + if (slot == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hub = slot->hub; + if (hub == NULL) { + return HYBBX_ERR_INVALID; + } + + if (slot->balance != NULL && slot->profile_set && + hub->config.balance.enabled) { + bctx.slot = slot; + fctx.hub = hub; + fctx.slot = slot; + return hybbx_circuit_balance_submit(slot->balance, frame, len, + balance_send_raw_cb, &bctx, + balance_flow_ctrl_cb, &fctx); + } + + return circuit_slot_send_raw(slot, frame, len); +} + +hybbx_result_t hybbx_circuit_hub_send_raw(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len) +{ + unsigned i; + + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE && hub->slots[i].fd >= 0) { + return circuit_slot_send_raw(&hub->slots[i], frame, len); + } + } + + return HYBBX_ERR_BUSY; +} + +static hybbx_result_t circuit_transport_write(hybbx_session_t *session, + const char *data, size_t len) +{ + hybbx_circuit_link_slot_t *slot; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + + if (session == NULL || data == NULL || len == 0) { + return HYBBX_OK; + } + + slot = (hybbx_circuit_link_slot_t *)session->transport_data; + if (slot == NULL) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_terminal(data, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return circuit_slot_send_hbx(slot, frame, frame_len); +} + +hybbx_result_t hybbx_circuit_hub_send_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len) +{ + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_circuit_hub_multicast_hbx(hub, frame, len, 0.0, 0, NULL); +} + +unsigned hybbx_circuit_hub_active_link_count(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE) { + count++; + } + } + + return count; +} + +static double circuit_slot_effective_frequency_mhz( + const hybbx_circuit_link_slot_t *slot, + const hybbx_circuit_hub_t *hub) +{ + double mhz; + + if (slot == NULL) { + return 0.0; + } + + mhz = slot->profile.frequency_mhz; + if (mhz <= 0.0 && slot->link_id[0] != '\0' && hub != NULL) { + const hybbx_circuit_bridge_entry_t *be = + hybbx_circuit_bridge_find(&hub->bridge, slot->link_id); + + if (be != NULL && be->frequency_mhz > 0.0) { + mhz = be->frequency_mhz; + } + } + + return mhz; +} + +hybbx_result_t hybbx_circuit_hub_multicast_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len, + double frequency_mhz, + int require_broadcast_qos, + unsigned *sent_out) +{ + unsigned i; + int sent = 0; + hybbx_result_t last_err = HYBBX_ERR_NOT_FOUND; + + if (sent_out != NULL) { + *sent_out = 0; + } + + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + hybbx_result_t rc; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (require_broadcast_qos && !circuit_slot_broadcast_qos(slot)) { + continue; + } + if (frequency_mhz > 0.0) { + double slot_mhz = circuit_slot_effective_frequency_mhz(slot, hub); + + if (slot_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, slot_mhz)) { + continue; + } + } + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len) && + !circuit_slot_can_send_low_prio(hub, slot)) { + last_err = HYBBX_ERR_BUSY; + continue; + } + + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len)) { + /* + * Low-priority AX.25 broadcast: admission is decided by balancer + * flow-control state, then transmit immediately (no queue slot). + * This avoids enqueue->break drops that can otherwise log as sent + * without reaching RF. + */ + rc = circuit_slot_send_raw(slot, frame, len); + if (rc == HYBBX_OK) { + hybbx_circuit_hub_note_rf_activity(hub, slot->link_id); + } + } else { + rc = circuit_slot_send_hbx(slot, frame, len); + } + if (rc == HYBBX_OK) { + sent++; + } else { + last_err = rc; + } + } + + if (sent > 0) { + if (sent_out != NULL) { + *sent_out = (unsigned)sent; + } + return HYBBX_OK; + } + + if (require_broadcast_qos) { + if (last_err != HYBBX_ERR_NOT_FOUND) { + return last_err; + } + return HYBBX_ERR_DENIED; + } + + return last_err; +} + +hybbx_result_t hybbx_circuit_hub_send_hbx_slot(hybbx_circuit_hub_t *hub, + unsigned slot_index, + const uint8_t *frame, size_t len, + int require_broadcast_qos) +{ + hybbx_circuit_link_slot_t *slot; + hybbx_result_t rc; + + if (hub == NULL || frame == NULL || len == 0 || + slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return HYBBX_ERR_INVALID; + } + + slot = &hub->slots[slot_index]; + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + return HYBBX_ERR_DENIED; + } + if (require_broadcast_qos && !circuit_slot_broadcast_qos(slot)) { + return HYBBX_ERR_DENIED; + } + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len) && + !circuit_slot_can_send_low_prio(hub, slot)) { + return HYBBX_ERR_BUSY; + } + + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len)) { + rc = circuit_slot_send_raw(slot, frame, len); + if (rc == HYBBX_OK) { + hybbx_circuit_hub_note_rf_activity(hub, slot->link_id); + } + } else { + rc = circuit_slot_send_hbx(slot, frame, len); + } + + return rc; +} + +double hybbx_circuit_hub_link_frequency_mhz(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return 0.0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE && hub->slots[i].profile_set) { + return hub->slots[i].profile.frequency_mhz; + } + } + + return 0.0; +} + +int hybbx_circuit_hub_link_broadcast_qos(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE && + circuit_slot_broadcast_qos(&hub->slots[i])) { + return 1; + } + } + + return 0; +} + +static void circuit_broadcast_links_sort(hybbx_circuit_broadcast_link_t *links, + unsigned count) +{ + unsigned i; + unsigned j; + + for (i = 1; i < count; i++) { + hybbx_circuit_broadcast_link_t key = links[i]; + + j = i; + while (j > 0) { + double prev_mhz = links[j - 1].frequency_mhz; + double key_mhz = key.frequency_mhz; + int move = 0; + + if (key_mhz > 0.0 && prev_mhz <= 0.0) { + move = 0; + } else if (key_mhz <= 0.0 && prev_mhz > 0.0) { + move = 1; + } else if (key_mhz > 0.0 && prev_mhz > 0.0 && + key_mhz < prev_mhz) { + move = 1; + } else if (key_mhz <= 0.0 && prev_mhz <= 0.0 && + strcmp(key.link_id, links[j - 1].link_id) < 0) { + move = 1; + } + + if (!move) { + break; + } + + links[j] = links[j - 1]; + j--; + } + links[j] = key; + } +} + +unsigned hybbx_circuit_hub_broadcast_links(const hybbx_circuit_hub_t *hub, + hybbx_circuit_broadcast_link_t *out, + unsigned out_max) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL || out == NULL || out_max == 0) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + const hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (!circuit_slot_broadcast_qos(slot)) { + continue; + } + if (count >= out_max) { + break; + } + + out[count].frequency_mhz = + circuit_slot_effective_frequency_mhz(slot, hub); + out[count].slot_index = i; + hybbx_strlcpy(out[count].link_id, slot->link_id, + sizeof(out[count].link_id)); + count++; + } + + if (count > 1) { + circuit_broadcast_links_sort(out, count); + } + + return count; +} + +static void on_circuit_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + hybbx_circuit_link_slot_t *slot = (hybbx_circuit_link_slot_t *)userdata; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + + (void)flags; + + if (slot == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_MAIL || + proto == HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + if (slot->hub != NULL && slot->hub->service != NULL) { + hybbx_mains_proxy_inbound_frame(slot->hub->service, proto, + payload, len); + } + return; + } + + if (slot->session == NULL || len == 0) { + return; + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_AX25: { + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, + sizeof(ui)); + if (ui_len > 0) { + (void)hybbx_session_handle_input(slot->session, ui, ui_len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_AX25_UI: { + size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, + ui, sizeof(ui)); + if (ui_len > 0) { + (void)hybbx_session_handle_input(slot->session, ui, ui_len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_TERMINAL: + (void)hybbx_session_handle_input(slot->session, payload, len); + break; + default: + hybbx_log_debug("[circuit] link=%s ignored proto=%s (%u bytes)", + slot->link_id[0] != '\0' ? slot->link_id : "?", + hybbx_circuit_proto_name(proto), (unsigned)len); + break; + } +} + +static void circuit_slot_reset(hybbx_circuit_link_slot_t *slot) +{ + if (slot == NULL) { + return; + } + + if (slot->balance != NULL) { + hybbx_circuit_balance_destroy(slot->balance); + slot->balance = NULL; + } + + if (slot->session != NULL) { + hybbx_session_close(slot->session); + slot->session = NULL; + } + + slot->profile_set = 0; + memset(&slot->profile, 0, sizeof(slot->profile)); + slot->link_id[0] = '\0'; + slot->last_rf_activity = 0; + slot->state = CIRCUIT_SLOT_FREE; + slot->thread = (pthread_t)0; +} + +static void circuit_close_slot(hybbx_circuit_link_slot_t *slot) +{ + hybbx_circuit_hub_t *hub; + int fd; + + if (slot == NULL || slot->hub == NULL) { + return; + } + + hub = slot->hub; + + pthread_mutex_lock(&hub->lock); + fd = slot->fd; + if (fd >= 0) { + close(fd); + slot->fd = -1; + } + pthread_mutex_unlock(&hub->lock); + + circuit_slot_reset(slot); +} + +static void circuit_close_all_slots(hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE || hub->slots[i].fd >= 0) { + circuit_close_slot(&hub->slots[i]); + } + } +} + +typedef struct circuit_auth_ctx { + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + int done; + int ok; + hybbx_link_auth_t auth; +} circuit_auth_ctx_t; + +static void circuit_evict_stale_link(hybbx_circuit_hub_t *hub, + hybbx_circuit_link_slot_t *stale, + const char *link_id) +{ + int fd = -1; + + if (hub == NULL || stale == NULL) { + return; + } + + /* + * Reconnect race: close the stale TCP fd and drop link_id so the new + * auth can proceed. Do not reset session/balance here — the stale link + * thread still owns those until it exits and calls circuit_close_slot. + */ + pthread_mutex_lock(&hub->lock); + fd = stale->fd; + if (fd >= 0) { + (void)shutdown(fd, SHUT_RDWR); + close(fd); + stale->fd = -1; + } + stale->link_id[0] = '\0'; + pthread_mutex_unlock(&hub->lock); + + if (link_id != NULL && link_id[0] != '\0') { + hybbx_log_info("[circuit] replacing stale link id=%s (reconnect)", + link_id); + } +} + +static int circuit_auth_fail_counts_as_abuse(const char *reason) +{ + if (reason == NULL) { + return 0; + } + + /* + * duplicate_id is a reconnect race (stale hub slot), not brute-force. + * circuit_validate_link_auth evicts the stale slot; do not ban for it. + */ + if (strcmp(reason, "duplicate_id") == 0) { + return 0; + } + + return 1; +} + +static void circuit_log_auth_fail(int fd, const char *reason, const char *id) +{ + char ip[HYBBX_REMOTE_ADDR_MAX]; + int count_abuse = circuit_auth_fail_counts_as_abuse(reason); + + if (reason == NULL) { + return; + } + + if (fd >= 0 && hybbx_socket_peer_name(fd, ip, sizeof(ip)) == HYBBX_OK) { + if (id != NULL && id[0] != '\0') { + hybbx_security_log_write( + "link_auth_fail ip=%s id=%s reason=%s transport=circuit", + ip, id, reason); + } else { + hybbx_security_log_write( + "link_auth_fail ip=%s reason=%s transport=circuit", + ip, reason); + } + if (count_abuse) { + hybbx_security_ban_link_auth_fail(ip); + if (id != NULL && id[0] != '\0' && strcmp(reason, "banned") != 0) { + hybbx_security_ban_link_auth_fail_callid(id); + } + } + } else { + hybbx_security_log_write( + "link_auth_fail ip=? reason=%s transport=circuit", reason); + } +} + +static int circuit_auth_password_ok(const hybbx_circuit_hub_t *hub, + const hybbx_circuit_bridge_entry_t *entry, + const char *password) +{ + if (hub == NULL || password == NULL) { + return 0; + } + + if (entry != NULL && entry->link_password[0] != '\0') { + return hybbx_password_match(entry->link_password, password); + } + + if (hub->link_password[0] != '\0') { + return hybbx_password_match(hub->link_password, password); + } + + return 0; +} + +static int circuit_validate_link_auth(circuit_auth_ctx_t *ctx, const char **reason) +{ + const hybbx_circuit_bridge_entry_t *entry = NULL; + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + + if (ctx == NULL || ctx->hub == NULL || ctx->slot == NULL) { + if (reason != NULL) { + *reason = "invalid"; + } + return 0; + } + + hub = ctx->hub; + slot = ctx->slot; + + if (!hub->link_auth) { + return 1; + } + + if (hub->bridge.count > 0) { + entry = hybbx_circuit_bridge_find(&hub->bridge, ctx->auth.id); + if (entry == NULL) { + if (reason != NULL) { + *reason = "unknown_id"; + } + return 0; + } + } else if (hub->link_password[0] == '\0') { + if (reason != NULL) { + *reason = "no_password"; + } + return 0; + } + + if (!circuit_auth_password_ok(hub, entry, ctx->auth.password)) { + if (reason != NULL) { + *reason = "password"; + } + return 0; + } + + { + int stale_idx = circuit_find_active_slot_by_id(hub, ctx->auth.id, slot); + + if (stale_idx >= 0) { + circuit_evict_stale_link(hub, &hub->slots[(unsigned)stale_idx], + ctx->auth.id); + if (circuit_find_active_slot_by_id(hub, ctx->auth.id, slot) >= 0) { + if (reason != NULL) { + *reason = "duplicate_id"; + } + return 0; + } + } + } + + (void)entry; + (void)slot; + return 1; +} + +static void circuit_on_auth_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + circuit_auth_ctx_t *ctx = (circuit_auth_ctx_t *)userdata; + const hybbx_circuit_bridge_entry_t *entry; + char code[HYBBX_LINK_CODE_MAX]; + char ack[HYBBX_LINK_AUTH_PAYLOAD_MAX]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + const char *fail_reason = "invalid"; + int fd; + + (void)flags; + + if (ctx == NULL || ctx->done || ctx->slot == NULL) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH) { + return; + } + + fd = ctx->slot->fd; + + if (hybbx_link_auth_parse((const char *)payload, len, &ctx->auth) != HYBBX_OK) { + circuit_log_auth_fail(fd, "invalid", NULL); + ctx->done = 1; + ctx->ok = 0; + return; + } + + if (!hybbx_security_ban_callid_accept(ctx->auth.id)) { + circuit_log_auth_fail(fd, "banned", ctx->auth.id); + ctx->done = 1; + ctx->ok = 0; + return; + } + + if (!circuit_validate_link_auth(ctx, &fail_reason)) { + hybbx_log_warn("[circuit] link auth failed for id=%s (%s)", + ctx->auth.id, fail_reason); + circuit_log_auth_fail(fd, fail_reason, ctx->auth.id); + ctx->done = 1; + ctx->ok = 0; + return; + } + + code[0] = '\0'; + (void)hybbx_link_registry_touch(&ctx->hub->links, ctx->auth.id, + ctx->auth.role, code, sizeof(code)); + + snprintf(ack, sizeof(ack), "ok=yes\nid=%s\ncode=%s\n", + ctx->auth.id, code[0] != '\0' ? code : "-"); + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK, + ack, strlen(ack), + frame, sizeof(frame)); + if (frame_len > 0) { + (void)circuit_slot_send_raw(ctx->slot, frame, frame_len); + } + + hybbx_strlcpy(ctx->slot->link_id, ctx->auth.id, sizeof(ctx->slot->link_id)); + + hybbx_circuit_link_profile_from_auth(&ctx->auth, &ctx->slot->profile); + entry = hybbx_circuit_bridge_find(&ctx->hub->bridge, ctx->auth.id); + if (entry != NULL && entry->frequency_mhz > 0.0 && + ctx->slot->profile.frequency_mhz <= 0.0) { + ctx->slot->profile.frequency_mhz = entry->frequency_mhz; + } + + if (ctx->slot->balance != NULL) { + hybbx_circuit_balance_set_profile(ctx->slot->balance, &ctx->slot->profile); + } + ctx->slot->profile_set = 1; + + hybbx_log_info("[circuit] link authenticated id=%s role=%s code=%s", + ctx->auth.id, ctx->auth.role, code[0] != '\0' ? code : "-"); + { + char qos_msg[128]; + int qos_len; + + qos_len = snprintf(qos_msg, sizeof(qos_msg), + "[circuit] link QoS bandwidth=%s baud=%u duplex=%s", + ctx->slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW ? + "low" : "high", + ctx->slot->profile.baud, + ctx->slot->profile.duplex == HYBBX_CIRCUIT_DUPLEX_HALF ? + "half" : "full"); + if (qos_len > 0 && ctx->slot->profile.frequency_mhz > 0.0) { + snprintf(qos_msg + (size_t)qos_len, + sizeof(qos_msg) - (size_t)qos_len, + " %.3fMHz", ctx->slot->profile.frequency_mhz); + } + hybbx_log_info("%s", qos_msg); + } + + ctx->done = 1; + ctx->ok = 1; +} + +static int circuit_wait_link_auth(hybbx_circuit_link_slot_t *slot) +{ + circuit_auth_ctx_t ctx; + hybbx_circuit_decoder_t dec; + uint8_t buf[256]; + unsigned elapsed = 0; + hybbx_circuit_hub_t *hub; + + if (slot == NULL || slot->hub == NULL) { + return 0; + } + + hub = slot->hub; + + if (!hub->link_auth) { + return 1; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.hub = hub; + ctx.slot = slot; + hybbx_circuit_decoder_init(&dec); + + while (!ctx.done && elapsed < HYBBX_CIRCUIT_AUTH_TIMEOUT_MS) { + struct pollfd pfd; + ssize_t n; + int pr; + + pthread_mutex_lock(&hub->lock); + pfd.fd = slot->fd; + pthread_mutex_unlock(&hub->lock); + + if (pfd.fd < 0) { + return 0; + } + + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + return 0; + } + if (pr == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + if ((pfd.revents & POLLIN) == 0) { + return 0; + } + + n = recv(pfd.fd, buf, sizeof(buf), 0); + if (n <= 0) { + return 0; + } + + hybbx_circuit_decoder_feed(&dec, buf, (size_t)n, + circuit_on_auth_frame, &ctx); + } + + if (!ctx.done) { + circuit_log_auth_fail(slot->fd, "timeout_no_link_auth", NULL); + } + + return ctx.ok; +} + +static void *circuit_link_thread(void *arg) +{ + hybbx_circuit_link_slot_t *slot = (hybbx_circuit_link_slot_t *)arg; + hybbx_circuit_hub_t *hub; + uint8_t buf[512]; + hybbx_result_t rc; + + if (slot == NULL || slot->hub == NULL) { + return NULL; + } + + hub = slot->hub; + + if (!circuit_wait_link_auth(slot)) { + hybbx_log_warn("[circuit] link authentication failed or timed out"); + circuit_close_slot(slot); + return NULL; + } + + if (!slot->profile_set) { + hybbx_circuit_link_profile_from_auth(NULL, &slot->profile); + if (slot->balance != NULL) { + hybbx_circuit_balance_set_profile(slot->balance, &slot->profile); + } + slot->profile_set = 1; + } + + hybbx_circuit_decoder_init(&slot->decoder); + + rc = hybbx_session_open(hub->service, &hybbx_plugin_circuit, slot, + &slot->session); + if (rc != HYBBX_OK) { + hybbx_log_warn("[circuit] session open failed for link %s", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + circuit_close_slot(slot); + return NULL; + } + + { + char remote[HYBBX_REMOTE_ADDR_MAX]; + int fd = slot->fd; + + if (fd >= 0 && + hybbx_socket_peer_name(fd, remote, sizeof(remote)) == HYBBX_OK) { + (void)hybbx_session_set_remote(slot->session, remote); + } + } + + slot->state = CIRCUIT_SLOT_ACTIVE; + slot->last_rf_activity = time(NULL); + hybbx_log_info("[circuit] link adapter attached id=%s (HBX bridge active)", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + + while (hub->running) { + struct pollfd pfd; + ssize_t n; + int pr; + + pthread_mutex_lock(&hub->lock); + pfd.fd = slot->fd; + pthread_mutex_unlock(&hub->lock); + + if (pfd.fd < 0) { + break; + } + + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + int cancel_link = 0; + + circuit_balance_tick_slot(slot, &cancel_link); + if (cancel_link) { + break; + } + if (slot->session != NULL) { + rc = hybbx_session_tick(slot->session); + if (rc == HYBBX_SESSION_END) { + break; + } + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + n = recv(pfd.fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + hybbx_circuit_decoder_feed(&slot->decoder, buf, (size_t)n, + on_circuit_frame, slot); + + { + int cancel_link = 0; + circuit_balance_tick_slot(slot, &cancel_link); + if (cancel_link) { + break; + } + } + } + + hybbx_log_info("[circuit] link detached id=%s", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + circuit_close_slot(slot); + return NULL; +} + +static void *circuit_accept_thread(void *arg) +{ + hybbx_circuit_hub_t *hub = (hybbx_circuit_hub_t *)arg; + + while (hub->running) { + struct pollfd pfds[2]; + int count = 0; + int pr; + int i; + + pfds[0].fd = hub->listen_v4; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + if (hub->listen_v4 >= 0) { + count = 1; + } + + pfds[1].fd = hub->listen_v6; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + if (hub->listen_v6 >= 0) { + count = 2; + } + + if (count == 0) { + break; + } + + pr = poll(pfds, (nfds_t)count, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + continue; + } + + for (i = 0; i < count; i++) { + if ((pfds[i].revents & POLLIN) == 0) { + continue; + } + + { + int client = accept(pfds[i].fd, NULL, NULL); + int slot_idx; + hybbx_circuit_link_slot_t *slot; + + if (client < 0) { + continue; + } + + (void)set_socket_options(client, 0); + + if (!hybbx_security_ban_accept_fd(client)) { + close(client); + continue; + } + + pthread_mutex_lock(&hub->lock); + if (circuit_count_used_slots(hub) >= hub->max_links) { + pthread_mutex_unlock(&hub->lock); + hybbx_log_warn("[circuit] max_links=%u reached — rejecting connection", + hub->max_links); + close(client); + continue; + } + + slot_idx = circuit_find_free_slot(hub); + if (slot_idx < 0) { + pthread_mutex_unlock(&hub->lock); + close(client); + continue; + } + + slot = &hub->slots[slot_idx]; + slot->fd = client; + slot->state = CIRCUIT_SLOT_CONNECTING; + slot->balance = hybbx_circuit_balance_create(&hub->config.balance); + if (slot->balance == NULL) { + hybbx_log_warn("[circuit] balance alloc failed — rejecting link"); + close(client); + slot->fd = -1; + slot->state = CIRCUIT_SLOT_FREE; + pthread_mutex_unlock(&hub->lock); + continue; + } + pthread_mutex_unlock(&hub->lock); + + if (pthread_create(&slot->thread, NULL, circuit_link_thread, + slot) != 0) { + hybbx_log_warn("[circuit] link thread failed"); + circuit_close_slot(slot); + } else { + pthread_detach(slot->thread); + } + } + } + } + + return NULL; +} + +hybbx_circuit_hub_t *hybbx_circuit_hub_create(hybbx_service_t *service) +{ + hybbx_circuit_hub_t *hub; + unsigned i; + + hub = calloc(1, sizeof(*hub)); + if (hub == NULL) { + return NULL; + } + + hub->service = service; + hub->listen_v4 = -1; + hub->listen_v6 = -1; + hub->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + pthread_mutex_init(&hub->lock, NULL); + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hub->slots[i].hub = hub; + hub->slots[i].fd = -1; + hub->slots[i].state = CIRCUIT_SLOT_FREE; + } + + return hub; +} + +void hybbx_circuit_hub_destroy(hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return; + } + + hybbx_circuit_hub_stop(hub); + pthread_mutex_destroy(&hub->lock); + free(hub); +} + +hybbx_result_t hybbx_circuit_hub_start(hybbx_circuit_hub_t *hub, + const hybbx_circuit_config_t *cfg) +{ + int backlog; + + if (hub == NULL || cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_circuit_hub_stop(hub); + hub->config = *cfg; + hub->bridge = cfg->bridge; + hub->max_links = cfg->max_links; + if (hub->max_links == 0 || hub->max_links > HYBBX_CIRCUIT_MAX_LINKS) { + hub->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + } + hybbx_strlcpy(hub->link_password, cfg->link_password, sizeof(hub->link_password)); + hub->link_auth = cfg->link_auth; + hybbx_link_registry_init(&hub->links, cfg->data_path, cfg->config_path, + cfg->link_stale_days); + (void)hybbx_link_registry_prune(&hub->links); + hub->running = 1; + + backlog = (int)hub->max_links; + if (backlog < 4) { + backlog = 4; + } + + if (cfg->ipv4) { + hub->listen_v4 = create_listen_socket(AF_INET, cfg->bind4, cfg->port, + backlog); + if (hub->listen_v4 < 0) { + hybbx_socket_log_bind_failure("circuit", cfg->bind4, cfg->port); + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + } + + if (cfg->ipv6) { + hub->listen_v6 = create_listen_socket(AF_INET6, cfg->bind6, cfg->port, + backlog); + if (hub->listen_v6 < 0) { + hybbx_log_warn("[circuit] IPv6 bind [%s]:%u skipped (%s)", + cfg->bind6, cfg->port, strerror(errno)); + } + } + + if (hub->listen_v4 < 0 && hub->listen_v6 < 0) { + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + + { + char hub_msg[256]; + size_t off = 0; + + off = (size_t)snprintf(hub_msg, sizeof(hub_msg), + "[circuit] internal TCP hub"); + if (hub->listen_v4 >= 0) { + off += (size_t)snprintf(hub_msg + off, sizeof(hub_msg) - off, + " %s:%u", cfg->bind4, cfg->port); + } + if (hub->listen_v6 >= 0) { + off += (size_t)snprintf(hub_msg + off, sizeof(hub_msg) - off, + " [%s]:%u", cfg->bind6, cfg->port); + } + snprintf(hub_msg + off, sizeof(hub_msg) - off, + " (HBX v%u, max_links=%u, bridge=%u)", + (unsigned)HYBBX_CIRCUIT_VERSION, hub->max_links, + hub->bridge.count); + hybbx_log_info("%s", hub_msg); + } + + if (pthread_create(&hub->accept_thread, NULL, circuit_accept_thread, + hub) != 0) { + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +void hybbx_circuit_hub_stop(hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return; + } + + hybbx_broadcast_ax25_seq_cancel(); + hub->running = 0; + + if (hub->listen_v4 >= 0) { + close(hub->listen_v4); + hub->listen_v4 = -1; + } + if (hub->listen_v6 >= 0) { + close(hub->listen_v6); + hub->listen_v6 = -1; + } + + circuit_close_all_slots(hub); + + if (hub->accept_thread) { + pthread_join(hub->accept_thread, NULL); + hub->accept_thread = (pthread_t)0; + } +} + +int hybbx_circuit_hub_running(const hybbx_circuit_hub_t *hub) +{ + return hub != NULL && hub->running; +} + +unsigned hybbx_circuit_hub_port(const hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return HYBBX_CIRCUIT_DEFAULT_PORT; + } + + return hub->config.port; +} + +void hybbx_circuit_hub_prune_links(hybbx_circuit_hub_t *hub) +{ + if (hub != NULL) { + (void)hybbx_link_registry_prune(&hub->links); + } +} + +void hybbx_circuit_hub_note_rf_activity(hybbx_circuit_hub_t *hub, + const char *link_id) +{ + time_t now; + unsigned i; + + if (hub == NULL || link_id == NULL || link_id[0] == '\0') { + return; + } + + now = time(NULL); + + pthread_mutex_lock(&hub->lock); + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (strcmp(slot->link_id, link_id) != 0) { + continue; + } + + slot->last_rf_activity = now; + break; + } + pthread_mutex_unlock(&hub->lock); +} + +int hybbx_circuit_hub_link_band_idle(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec) +{ + const hybbx_circuit_link_slot_t *slot; + hybbx_circuit_hub_t *mutable_hub; + time_t now; + time_t last_rf; + time_t idle_since; + circuit_slot_state_t state; + int fd; + + if (hub == NULL || slot_index >= HYBBX_CIRCUIT_MAX_LINKS || + min_idle_sec == 0) { + return 0; + } + + mutable_hub = (hybbx_circuit_hub_t *)hub; + pthread_mutex_lock(&mutable_hub->lock); + slot = &hub->slots[slot_index]; + state = slot->state; + fd = slot->fd; + last_rf = slot->last_rf_activity; + pthread_mutex_unlock(&mutable_hub->lock); + + if (state != CIRCUIT_SLOT_ACTIVE || fd < 0) { + return 0; + } + if (last_rf == 0) { + return 1; + } + + now = time(NULL); + idle_since = now - last_rf; + if (idle_since < 0) { + return 0; + } + + return (unsigned)idle_since >= min_idle_sec; +} + +time_t hybbx_circuit_hub_link_band_ready_at(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec) +{ + hybbx_circuit_hub_t *mutable_hub; + const hybbx_circuit_link_slot_t *slot; + circuit_slot_state_t state; + int fd; + time_t last_rf; + time_t now; + + if (hub == NULL || slot_index >= HYBBX_CIRCUIT_MAX_LINKS || + min_idle_sec == 0) { + return (time_t)-1; + } + + mutable_hub = (hybbx_circuit_hub_t *)hub; + pthread_mutex_lock(&mutable_hub->lock); + slot = &hub->slots[slot_index]; + state = slot->state; + fd = slot->fd; + last_rf = slot->last_rf_activity; + pthread_mutex_unlock(&mutable_hub->lock); + + if (state != CIRCUIT_SLOT_ACTIVE || fd < 0) { + return (time_t)-1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return (time_t)-1; + } + if (last_rf == 0) { + return now; + } + + if ((unsigned)(now - last_rf) >= min_idle_sec) { + return now; + } + + return last_rf + (time_t)min_idle_sec; +} + +static hybbx_result_t circuit_plugin_init(hybbx_service_t *service) +{ + (void)service; + return HYBBX_OK; +} + +static void circuit_plugin_shutdown(void) +{ +} + +static hybbx_result_t circuit_plugin_start(const char *config) +{ + (void)config; + return HYBBX_OK; +} + +static hybbx_result_t circuit_plugin_stop(void) +{ + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_circuit = { + .name = "circuit", + .kind = HYBBX_TRANSPORT_CIRCUIT, + .version = 1, + .init = circuit_plugin_init, + .shutdown = circuit_plugin_shutdown, + .start = circuit_plugin_start, + .stop = circuit_plugin_stop, + .write = circuit_transport_write, +}; diff --git a/src/core/command.c b/src/core/command.c new file mode 100644 index 0000000..b60ee35 --- /dev/null +++ b/src/core/command.c @@ -0,0 +1,2450 @@ +#include "hybbx/hybbx.h" +#include "hybbx/command.h" +#include "hybbx/commands_registry.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/texts.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/chat.h" +#include "hybbx/conference.h" +#include "hybbx/mail.h" +#include "hybbx/proxymail.h" +#include "hybbx/proxychat.h" +#include "hybbx/broadcast.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/service.h" +#include "hybbx/password.h" +#include "hybbx/traffic.h" +#include "hybbx/monitor.h" +#include "hybbx/util.h" + +#include +#include +#include +#include + +static hybbx_result_t cmd_proxymail(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd); +static hybbx_result_t cmd_proxychat(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd); + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void cmd_deny_privilege(hybbx_session_t *session); + +static const char *cmd_help_unknown(hybbx_session_t *session) +{ + if (hybbx_session_is_guest(session) || + (hybbx_session_login_prompt(session) && + !hybbx_session_logged_in(session))) { + return "Unknown command. Try /help."; + } + + return "Unknown command. /help for list."; +} + +static int cmd_verb_allowed_login_prompt(const char *verb) +{ + if (verb == NULL || verb[0] == '\0') { + return 1; + } + + if (hybbx_commands_registry_verb_allowed(HYBBX_LEVEL_GUEST, verb)) { + return 1; + } + + return str_ieq(verb, "exit") || str_ieq(verb, "logout") || + str_ieq(verb, "bye") || str_ieq(verb, "quit"); +} + +static hybbx_result_t cmd_check_access(hybbx_session_t *session, + const char *verb) +{ + hybbx_user_level_t level = hybbx_session_user_level(session); + + if (!hybbx_session_logged_in(session) && + hybbx_session_login_prompt(session)) { + if (cmd_verb_allowed_login_prompt(verb)) { + return HYBBX_OK; + } + hybbx_session_write_line(session, + "Log in with /login or use /register."); + return HYBBX_ERR_DENIED; + } + + if (hybbx_commands_registry_verb_allowed(level, verb)) { + return HYBBX_OK; + } + + /* Optional [monitor] allow= grants beyond commands.yaml min level. */ + if ((str_ieq(verb, "monitor") || str_ieq(verb, "mon")) && + hybbx_monitor_enabled() && + hybbx_monitor_user_allowed(hybbx_session_username(session))) { + return HYBBX_OK; + } + + if (level == HYBBX_LEVEL_GUEST) { + hybbx_session_write_line(session, + "Not available to guests. Try /help."); + } else if (str_ieq(verb, "register")) { + hybbx_session_write_line(session, + "Only guests may self-register with /register."); + } else if (str_ieq(verb, "changeme")) { + hybbx_session_write_line(session, + "Only registered users may use /changeme."); + } else { + hybbx_session_write_line(session, "Insufficient privileges."); + } + + return HYBBX_ERR_DENIED; +} + +static int cmd_deleteme_confirmed(const char *arg) +{ + return arg != NULL && hybbx_bool_is_true(arg); +} + +static void cmd_registry_usage(hybbx_session_t *session, const char *verb) +{ + const char *canonical = hybbx_commands_registry_canonical(verb); + + hybbx_commands_registry_show_help(session, canonical); +} + +static hybbx_result_t cmd_help_topic(hybbx_session_t *session, const char *topic) +{ + hybbx_user_level_t level = hybbx_session_user_level(session); + const char *canonical; + const hybbx_command_def_t *def; + + if (topic == NULL || topic[0] == '\0') { + hybbx_commands_registry_show_menu(session); + return HYBBX_OK; + } + + canonical = hybbx_commands_registry_canonical(topic); + + if (!hybbx_commands_registry_help_allowed(level, canonical)) { + /* Allow-list grants may read /help monitor without Sysop min. */ + if (!((str_ieq(canonical, "monitor") || str_ieq(canonical, "mon")) && + hybbx_monitor_session_may_use(session))) { + hybbx_session_write_line(session, cmd_help_unknown(session)); + return HYBBX_ERR_NOT_FOUND; + } + } + + def = hybbx_commands_registry_find(canonical); + if (def == NULL || def->line1[0] == '\0') { + hybbx_session_write_line(session, cmd_help_unknown(session)); + return HYBBX_ERR_NOT_FOUND; + } + + hybbx_commands_registry_show_help(session, canonical); + return HYBBX_OK; +} + +static hybbx_result_t cmd_help(hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + if (cmd->argc == 0) { + hybbx_commands_registry_show_menu(session); + return HYBBX_OK; + } + + return cmd_help_topic(session, cmd->argv[0]); +} + +static hybbx_result_t cmd_news(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_texts_send_file(texts, session, HYBBX_TEXT_NEWS) != HYBBX_OK) { + hybbx_session_write_line(session, "(no news available)"); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_banner(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + (void)hybbx_texts_send_banner(texts, session, HYBBX_VERSION_STRING, + hybbx_service_get_name(service)); + return HYBBX_OK; +} + +static hybbx_result_t cmd_motd(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_texts_send_motd(texts, session) != HYBBX_OK) { + hybbx_session_write_line(session, "(no motd available)"); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_rules(hybbx_service_t *service, hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_texts_send_file(texts, session, HYBBX_TEXT_RULES) != HYBBX_OK) { + hybbx_session_write_line(session, "(no rules available)"); + } + + return HYBBX_OK; +} + +static const char *who_transport_label(const char *transport) +{ + if (transport == NULL || transport[0] == '\0') { + return "unknown"; + } + + if (str_ieq(transport, "packet_radio")) { + return "ax25"; + } + if (str_ieq(transport, "telnet")) { + return "telnet"; + } + if (str_ieq(transport, "circuit")) { + return "circuit"; + } + if (str_ieq(transport, "ssh")) { + return "ssh"; + } + if (str_ieq(transport, "websocket")) { + return "websocket"; + } + + return transport; +} + +typedef struct who_ctx { + hybbx_session_t *requester; + unsigned count; + int at_plugin; +} who_ctx_t; + +static void who_list_visitor(hybbx_session_t *session, void *userdata) +{ + who_ctx_t *ctx = (who_ctx_t *)userdata; + const hybbx_session_record_t *rec; + const char *name; + const char *transport; + char line[96]; + char user_at[80]; + + if (ctx == NULL || session == NULL || !hybbx_session_logged_in(session)) { + return; + } + + if (!hybbx_session_is_interactive_user(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + if (ctx->at_plugin) { + if (hybbx_session_format_user_at_plugin(session, user_at, + sizeof(user_at)) != HYBBX_OK) { + return; + } + snprintf(line, sizeof(line), " %s", user_at); + } else { + name = hybbx_session_display_name(session); + rec = hybbx_session_record(session); + transport = who_transport_label(rec != NULL ? rec->transport : NULL); + if (rec == NULL || rec->transport[0] == '\0') { + transport = who_transport_label(session->transport != NULL ? + session->transport->name : NULL); + } + snprintf(line, sizeof(line), " %-16s %s", name, transport); + } + + hybbx_session_write_line(ctx->requester, line); + ctx->count++; +} + +static hybbx_result_t cmd_who(hybbx_service_t *service, hybbx_session_t *session) +{ + who_ctx_t ctx; + char total[32]; + + ctx.requester = session; + ctx.count = 0; + ctx.at_plugin = hybbx_service_login_announce(service); + + hybbx_session_write_line(session, "Online users:"); + hybbx_service_visit_sessions(service, who_list_visitor, &ctx); + + if (ctx.count == 0) { + hybbx_session_write_line(session, " (none)"); + } + + snprintf(total, sizeof(total), "Total: %u", ctx.count); + hybbx_session_write_line(session, total); + return HYBBX_OK; +} + +typedef struct users_stats_ctx { + size_t by_level[6]; + size_t total; +} users_stats_ctx_t; + +static hybbx_result_t users_stats_cb(const hybbx_user_record_t *user, void *ctx) +{ + users_stats_ctx_t *stats = (users_stats_ctx_t *)ctx; + + if (user == NULL || stats == NULL) { + return HYBBX_OK; + } + + if (user->level < HYBBX_LEVEL_SYSOP || user->level > HYBBX_LEVEL_USER) { + return HYBBX_OK; + } + + stats->by_level[user->level]++; + stats->total++; + return HYBBX_OK; +} + +static void users_compute_percents(const size_t *counts, size_t total, + unsigned *percents) +{ + hybbx_user_level_t level; + hybbx_user_level_t adjust_level = HYBBX_LEVEL_SYSOP; + unsigned sum = 0; + unsigned max_remainder = 0; + + if (total == 0) { + return; + } + + for (level = HYBBX_LEVEL_SYSOP; level <= HYBBX_LEVEL_USER; level++) { + unsigned scaled = (unsigned)((counts[level] * 1000u) / total); + unsigned rem = scaled % 10u; + + percents[level] = scaled / 10u; + sum += percents[level]; + if (rem > max_remainder) { + max_remainder = rem; + adjust_level = level; + } + } + + if (sum < 100u) { + percents[adjust_level] += 100u - sum; + } +} + +static hybbx_result_t cmd_users(hybbx_service_t *service, hybbx_session_t *session) +{ + hybbx_storage_t *storage; + users_stats_ctx_t stats; + unsigned percents[6]; + hybbx_user_level_t level; + char line[48]; + char total[40]; + hybbx_result_t rc; + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + hybbx_session_write_line(session, "User storage unavailable."); + return HYBBX_ERR_INVALID; + } + + memset(&stats, 0, sizeof(stats)); + memset(percents, 0, sizeof(percents)); + + rc = hybbx_storage_foreach_user(storage, users_stats_cb, &stats); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read user database."); + return rc; + } + + hybbx_session_write_line(session, "Registered users:"); + + if (stats.total == 0) { + hybbx_session_write_line(session, " (none)"); + } else { + users_compute_percents(stats.by_level, stats.total, percents); + for (level = HYBBX_LEVEL_SYSOP; level <= HYBBX_LEVEL_USER; level++) { + if (stats.by_level[level] == 0) { + continue; + } + + snprintf(line, sizeof(line), " %-8s %3u%%", + hybbx_user_level_name(level), percents[level]); + hybbx_session_write_line(session, line); + } + } + + snprintf(total, sizeof(total), "Total users: %zu", stats.total); + hybbx_session_write_line(session, total); + return HYBBX_OK; +} + +static hybbx_result_t cmd_session(hybbx_session_t *session) +{ + char buf[128]; + const hybbx_session_record_t *rec = hybbx_session_record(session); + + if (rec == NULL) { + return HYBBX_ERR_INVALID; + } + + snprintf(buf, sizeof(buf), "User: %s", hybbx_session_display_name(session)); + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Level: %s", + hybbx_user_level_name(hybbx_session_user_level(session))); + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Session: %llu", + (unsigned long long)rec->session_id); + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Transport: %s", rec->transport); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t parse_profile_fields(const hybbx_parsed_command_t *cmd, + unsigned name_start, + unsigned tail_extra, + hybbx_user_registration_t *reg) +{ + size_t i; + size_t pos = 0; + unsigned suffix; + + if (cmd == NULL || reg == NULL) { + return HYBBX_ERR_INVALID; + } + + suffix = 3u + tail_extra; + if (cmd->argc < name_start + suffix) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(reg->email, cmd->argv[cmd->argc - 1u - tail_extra], + sizeof(reg->email)); + hybbx_strlcpy(reg->location, cmd->argv[cmd->argc - 2u - tail_extra], + sizeof(reg->location)); + hybbx_strlcpy(reg->country, cmd->argv[cmd->argc - 3u - tail_extra], + sizeof(reg->country)); + + reg->full_name[0] = '\0'; + for (i = name_start; i + suffix < cmd->argc; i++) { + size_t part_len; + const char *part = cmd->argv[i]; + + if (part == NULL) { + continue; + } + + part_len = strlen(part); + if (pos > 0) { + if (pos + 1 >= sizeof(reg->full_name)) { + return HYBBX_ERR_INVALID; + } + reg->full_name[pos++] = ' '; + } + + if (pos + part_len >= sizeof(reg->full_name)) { + return HYBBX_ERR_INVALID; + } + + memcpy(reg->full_name + pos, part, part_len); + pos += part_len; + } + + reg->full_name[pos] = '\0'; + return HYBBX_OK; +} + +static hybbx_result_t parse_registration(const hybbx_parsed_command_t *cmd, + hybbx_user_registration_t *reg, + int with_password) +{ + hybbx_result_t rc; + unsigned min_args = with_password ? 6u : 5u; + unsigned tail_extra = with_password ? 1u : 0u; + + if (cmd == NULL || reg == NULL || cmd->argc < min_args) { + return HYBBX_ERR_INVALID; + } + + memset(reg, 0, sizeof(*reg)); + hybbx_strlcpy(reg->nickname, cmd->argv[0], sizeof(reg->nickname)); + hybbx_strlcpy(reg->username, cmd->argv[0], sizeof(reg->username)); + hybbx_username_normalize(reg->username); + + if (with_password) { + hybbx_strlcpy(reg->password, cmd->argv[cmd->argc - 1], + sizeof(reg->password)); + } + + rc = parse_profile_fields(cmd, 1, tail_extra, reg); + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_changeme(const hybbx_parsed_command_t *cmd, + const char *username, + hybbx_user_registration_t *reg, + const char **old_password, + const char **new_password) +{ + hybbx_result_t rc; + + if (cmd == NULL || username == NULL || reg == NULL || + old_password == NULL || new_password == NULL || cmd->argc < 6) { + return HYBBX_ERR_INVALID; + } + + memset(reg, 0, sizeof(*reg)); + hybbx_strlcpy(reg->username, username, sizeof(reg->username)); + hybbx_username_normalize(reg->username); + *old_password = cmd->argv[0]; + *new_password = cmd->argv[1]; + + rc = parse_profile_fields(cmd, 2, 0, reg); + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_userchange(const hybbx_parsed_command_t *cmd, + hybbx_user_registration_t *reg, + const char **new_password) +{ + hybbx_result_t rc; + + if (cmd == NULL || reg == NULL || new_password == NULL || cmd->argc < 7) { + return HYBBX_ERR_INVALID; + } + + memset(reg, 0, sizeof(*reg)); + hybbx_strlcpy(reg->username, cmd->argv[0], sizeof(reg->username)); + hybbx_username_normalize(reg->username); + *new_password = cmd->argv[1]; + + rc = parse_profile_fields(cmd, 2, 0, reg); + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_lookup_user(hybbx_service_t *service, + const char *username, + hybbx_user_record_t *out) +{ + hybbx_storage_t *storage; + hybbx_result_t rc; + + if (service == NULL || username == NULL || out == NULL || + username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, username, out); + if (rc == HYBBX_ERR_NOT_FOUND) { + return HYBBX_ERR_NOT_FOUND; + } + + return rc; +} + +static void cmd_deny_privilege(hybbx_session_t *session) +{ + hybbx_session_write_line(session, "Insufficient privileges."); +} + +static hybbx_result_t cmd_activate(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /activate "); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + if (!hybbx_commands_registry_verb_allowed(actor_level, "activate")) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (target.level != HYBBX_LEVEL_USER) { + hybbx_session_write_line(session, + "Only pending registered user accounts can be activated."); + return HYBBX_OK; + } + + if (target.active) { + hybbx_session_write_line(session, "Account is already active."); + return HYBBX_OK; + } + + target.active = 1; + rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Activated '%s'.", + hybbx_user_display_name(&target)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_promote(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + hybbx_user_level_t new_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 2 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0' || + cmd->argv[1] == NULL || cmd->argv[1][0] == '\0') { + hybbx_session_write_line(session, + "Usage: /promote admin|mod"); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + new_level = hybbx_user_level_parse(cmd->argv[1]); + if (new_level != HYBBX_LEVEL_ADMIN && new_level != HYBBX_LEVEL_MOD) { + hybbx_session_write_line(session, + "Level must be admin or mod."); + return HYBBX_OK; + } + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (!hybbx_commands_registry_may_promote(actor_level, target.level, target.active, + new_level)) { + if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) { + cmd_deny_privilege(session); + } else if (new_level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Only the Sysop may add Admins."); + } else { + hybbx_session_write_line(session, + "Only Sysop or Admin may add Mods."); + } + return HYBBX_ERR_DENIED; + } + + if (target.level == new_level) { + snprintf(buf, sizeof(buf), "'%s' is already %s.", + hybbx_user_display_name(&target), + hybbx_user_level_name(new_level)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; + } + + target.level = new_level; + rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Promoted '%s' to %s.", + hybbx_user_display_name(&target), + hybbx_user_level_name(new_level)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_demote(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + hybbx_user_level_t old_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /demote "); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(target.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, "You cannot demote your own account."); + return HYBBX_OK; + } + + old_level = target.level; + if (!hybbx_commands_registry_may_demote(actor_level, target.level)) { + if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) { + cmd_deny_privilege(session); + } else if (target.level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Only the Sysop may remove Admins."); + } else if (target.level == HYBBX_LEVEL_MOD) { + hybbx_session_write_line(session, + "Only Sysop or Admin may remove Mods."); + } else { + hybbx_session_write_line(session, + "That account is not an Admin or Mod."); + } + return HYBBX_ERR_DENIED; + } + + target.level = HYBBX_LEVEL_USER; + rc = hybbx_storage_update_user(hybbx_service_get_storage(service), &target); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Demoted '%s' from %s to user.", + hybbx_user_display_name(&target), + hybbx_user_level_name(old_level)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_delete(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + char buf[128]; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /delete "); + return HYBBX_OK; + } + + actor_level = hybbx_session_user_level(session); + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(target.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, "You cannot delete your own account."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_sysop(target.level)) { + hybbx_session_write_line(session, + "The Sysop account is permanent and cannot be deleted."); + return HYBBX_OK; + } + + if (!hybbx_commands_registry_may_delete(actor_level, target.level)) { + if (actor_level == HYBBX_LEVEL_MOD || actor_level == HYBBX_LEVEL_USER) { + cmd_deny_privilege(session); + } else if (target.level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Admins cannot delete other Admins."); + } else { + cmd_deny_privilege(session); + } + return HYBBX_ERR_DENIED; + } + + rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), target.id); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Deleted account '%s'.", + hybbx_user_display_name(&target)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_deleteme(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const hybbx_session_record_t *rec; + hybbx_user_level_t level; + const char *arg; + hybbx_result_t rc; + + (void)service; + + level = hybbx_session_user_level(session); + if (hybbx_user_level_is_sysop(level)) { + hybbx_session_write_line(session, + "The Sysop account is permanent and cannot be deleted."); + return HYBBX_OK; + } + + arg = cmd->argc > 0 ? cmd->argv[0] : NULL; + if (arg != NULL && hybbx_bool_is_false(arg)) { + hybbx_session_write_line(session, "Account deletion cancelled."); + return HYBBX_OK; + } + + if (!cmd_deleteme_confirmed(arg)) { + hybbx_session_write_line(session, + "Usage: /deleteme yes|no"); + return HYBBX_OK; + } + + rec = hybbx_session_record(session); + if (rec == NULL || rec->user_id == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), + rec->user_id); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Account not found."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, "Account deleted. Goodbye."); + return HYBBX_SESSION_END; +} + +static hybbx_result_t cmd_register_user(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd, + int staff_created) +{ + hybbx_storage_t *storage; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + char buf[HYBBX_USER_FULL_NAME_MAX + 64]; + hybbx_result_t rc; + + if (staff_created) { + if (cmd->argc < 5) { + hybbx_session_write_line(session, + "Usage: /usercreate "); + return HYBBX_OK; + } + } else if (cmd->argc < 6) { + hybbx_session_write_line(session, + "Usage: /register "); + return HYBBX_OK; + } + + rc = parse_registration(cmd, ®, staff_created ? 0 : 1); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Registration fields too long."); + return HYBBX_OK; + } + + if (!staff_created && !hybbx_password_plain_valid(reg.password)) { + hybbx_session_write_line(session, + "Password must be 8-24 characters (- not allowed)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_register_user(storage, ®, &user); + if (rc == HYBBX_ERR_BUSY) { + hybbx_session_write_line(session, "Username or nickname already taken."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, + "Invalid username (4-12 chars, one _ or one -, max 4 digits) or bad profile."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (staff_created) { + snprintf(buf, sizeof(buf), "User '%s' created.", + hybbx_user_display_name(&user)); + } else { + snprintf(buf, sizeof(buf), "Registration received for '%s'.", + hybbx_user_display_name(&user)); + } + hybbx_session_write_line(session, buf); + snprintf(buf, sizeof(buf), "Name: %s", user.full_name); + hybbx_session_write_line(session, buf); + if (staff_created) { + hybbx_session_write_line(session, + "Account is inactive. Use /activate before the user can log in."); + } else { + const hybbx_mail_config_t *mail_cfg = hybbx_service_get_mail(service); + + (void)hybbx_mail_notify_staff_registration(service, ®, &user); + hybbx_session_write_line(session, + "A Sysop or Admin must activate your account before you can log in."); + if (mail_cfg != NULL && mail_cfg->enabled) { + hybbx_session_write_line(session, + "Sysop and Admin were notified by mail."); + } + } + return HYBBX_OK; +} + +static hybbx_result_t cmd_register(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + if (!hybbx_commands_registry_verb_allowed(hybbx_session_user_level(session), + "register") && + !(hybbx_session_login_prompt(session) && + !hybbx_session_logged_in(session))) { + hybbx_session_write_line(session, + "Only guests or login-prompt sessions may self-register with /register."); + return HYBBX_ERR_DENIED; + } + + return cmd_register_user(service, session, cmd, 0); +} + +static hybbx_result_t cmd_createuser(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + return cmd_register_user(service, session, cmd, 1); +} + +static hybbx_result_t cmd_changeme(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_storage_t *storage; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + const char *old_password; + const char *new_password; + const char *username; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, + "Only registered users may use /changeme."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 6) { + hybbx_session_write_line(session, + "Usage: /changeme "); + return HYBBX_OK; + } + + username = hybbx_session_username(session); + if (username == NULL || username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = parse_changeme(cmd, username, ®, &old_password, &new_password); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Profile fields too long."); + return HYBBX_OK; + } + + if (!hybbx_password_plain_valid(new_password)) { + hybbx_session_write_line(session, + "New password must be 8-24 characters (- not allowed)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_find_user(storage, reg.username, &user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Account not found."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + { + const hybbx_session_record_t *rec = hybbx_session_record(session); + + if (rec == NULL || user.id != rec->user_id) { + hybbx_session_write_line(session, + "You can only change your own account."); + return HYBBX_OK; + } + + if (!hybbx_user_profile_valid(®)) { + hybbx_session_write_line(session, + "Invalid profile (check name, country, location, email)."); + return HYBBX_OK; + } + } + + if (user.password[0] == '\0') { + hybbx_session_write_line(session, + "Account has no password. Ask Sysop or Admin to set one."); + return HYBBX_OK; + } + + if (!hybbx_password_match(user.password, old_password)) { + hybbx_session_write_line(session, "Old password incorrect."); + return HYBBX_OK; + } + + hybbx_strlcpy(user.full_name, reg.full_name, sizeof(user.full_name)); + hybbx_strlcpy(user.country, reg.country, sizeof(user.country)); + hybbx_strlcpy(user.location, reg.location, sizeof(user.location)); + hybbx_strlcpy(user.email, reg.email, sizeof(user.email)); + + rc = hybbx_password_hash(new_password, user.password, sizeof(user.password)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Could not store new password."); + return rc; + } + + rc = hybbx_storage_update_user(storage, &user); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, "Account updated."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_userchange(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_storage_t *storage; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + const char *new_password; + hybbx_user_level_t actor_level; + hybbx_result_t rc; + char buf[160]; + + actor_level = hybbx_session_user_level(session); + if (!hybbx_user_level_is_sysop_or_admin(actor_level)) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 7) { + hybbx_session_write_line(session, + "Usage: /changeuser "); + return HYBBX_OK; + } + + rc = parse_userchange(cmd, ®, &new_password); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Profile fields too long."); + return HYBBX_OK; + } + + if (!hybbx_password_plain_valid(new_password)) { + hybbx_session_write_line(session, + "New password must be 8-24 characters (- not allowed)."); + return HYBBX_OK; + } + + if (!hybbx_user_profile_valid(®)) { + hybbx_session_write_line(session, + "Invalid profile (check name, country, location, email)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_find_user(storage, reg.username, &user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(user.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, + "Use /changeme to update your own account."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_sysop(user.level)) { + hybbx_session_write_line(session, + "The Sysop account cannot be changed with /changeuser."); + return HYBBX_OK; + } + + if (!hybbx_commands_registry_may_userchange(actor_level, user.level)) { + if (actor_level == HYBBX_LEVEL_ADMIN && + user.level == HYBBX_LEVEL_ADMIN) { + hybbx_session_write_line(session, + "Admins cannot change other Admins. Sysop only."); + } else { + cmd_deny_privilege(session); + } + return HYBBX_ERR_DENIED; + } + + hybbx_strlcpy(user.full_name, reg.full_name, sizeof(user.full_name)); + hybbx_strlcpy(user.country, reg.country, sizeof(user.country)); + hybbx_strlcpy(user.location, reg.location, sizeof(user.location)); + hybbx_strlcpy(user.email, reg.email, sizeof(user.email)); + + rc = hybbx_password_hash(new_password, user.password, sizeof(user.password)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Could not store new password."); + return rc; + } + + rc = hybbx_storage_update_user(storage, &user); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Updated account '%s'.", + hybbx_user_display_name(&user)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_userdelete(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_user_record_t target; + hybbx_user_level_t actor_level; + char buf[128]; + hybbx_result_t rc; + + actor_level = hybbx_session_user_level(session); + if (!hybbx_user_level_is_sysop(actor_level)) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, "Usage: /deleteuser "); + return HYBBX_OK; + } + + rc = cmd_lookup_user(service, cmd->argv[0], &target); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (str_ieq(target.username, hybbx_session_username(session))) { + hybbx_session_write_line(session, "You cannot delete your own account."); + return HYBBX_OK; + } + + if (!hybbx_commands_registry_may_userdelete(actor_level, target.level)) { + hybbx_session_write_line(session, + "The Sysop account is permanent and cannot be deleted."); + return HYBBX_OK; + } + + rc = hybbx_storage_delete_user(hybbx_service_get_storage(service), target.id); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(buf, sizeof(buf), "Deleted account '%s'.", + hybbx_user_display_name(&target)); + hybbx_session_write_line(session, buf); + return HYBBX_OK; +} + +static hybbx_result_t cmd_login(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_storage_t *storage; + hybbx_user_record_t user; + const hybbx_auth_config_t *auth; + const char *guest_prefix; + char username[HYBBX_USER_NAME_MAX]; + unsigned guest_slot; + hybbx_result_t rc; + + if (cmd->argc < 1 || cmd->argv[0] == NULL || cmd->argv[0][0] == '\0') { + hybbx_session_write_line(session, + "Usage: /login "); + return HYBBX_OK; + } + + auth = hybbx_service_get_auth(service); + guest_prefix = auth != NULL ? auth->guest_prefix : HYBBX_AUTH_DEFAULT_GUEST_PREFIX; + + hybbx_strlcpy(username, cmd->argv[0], sizeof(username)); + hybbx_username_normalize(username); + + if (hybbx_guest_slot_from_username(guest_prefix, username, &guest_slot)) { + if (auth != NULL && auth->auto_login) { + hybbx_session_write_line(session, + "Guest access is automatic on connect (auto_login). " + "/login is for registered accounts only."); + } else { + hybbx_session_write_line(session, + "/login is for registered accounts only."); + } + return HYBBX_OK; + } + + if (cmd->argc < 2 || cmd->argv[1] == NULL || cmd->argv[1][0] == '\0') { + hybbx_session_write_line(session, + "Usage: /login "); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_find_user(storage, username, &user); + + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (hybbx_user_level_is_guest(user.level)) { + hybbx_session_write_line(session, + "Guests are ephemeral and use auto-login on connect."); + return HYBBX_OK; + } + + if (!user.active) { + hybbx_session_write_line(session, + "Account pending activation by Sysop or Admin."); + return HYBBX_OK; + } + + if (user.password[0] == '\0') { + hybbx_session_write_line(session, + "Account has no password. Contact Sysop or Admin."); + return HYBBX_OK; + } + + if (!hybbx_password_match(user.password, cmd->argv[1])) { + const hybbx_session_record_t *rec = hybbx_session_record(session); + + hybbx_security_log_write("login_fail ip=%s user=%s transport=%s", + rec != NULL && rec->remote[0] != '\0' ? + rec->remote : "?", + username, + rec != NULL && rec->transport[0] != '\0' ? + rec->transport : "?"); + if (rec != NULL && rec->remote[0] != '\0') { + hybbx_security_ban_login_fail(rec->remote, rec->transport); + } + hybbx_session_write_line(session, "Invalid password."); + return HYBBX_OK; + } + + if (hybbx_service_find_registered_session(service, user.id, session) != + NULL) { + hybbx_session_write_line(session, + "That account is already logged in elsewhere."); + return HYBBX_OK; + } + + rc = hybbx_session_switch_user(session, &user); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Login failed."); + return rc; + } + + { + time_t since_login = user.last_login_at; + const hybbx_texts_config_t *texts; + + texts = hybbx_service_get_texts(service); + if (texts != NULL) { + (void)hybbx_texts_send_motd(texts, session); + } + + hybbx_mail_announce_since_last_login(service, session, since_login); + + user.last_login_at = time(NULL); + (void)hybbx_storage_update_user(storage, &user); + } + hybbx_session_show_prompt(session); + return HYBBX_OK; +} + +static hybbx_result_t cmd_chat(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const hybbx_chat_config_t *chat; + unsigned channel_index; + unsigned current; + const char *name; + char line[HYBBX_CHAT_CHANNEL_NAME_MAX + 48]; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use chat."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc > 0 && str_ieq(cmd->argv[0], "proxychat")) { + hybbx_parsed_command_t proxy_cmd; + char proxy_verb[] = "proxychat"; + + memset(&proxy_cmd, 0, sizeof(proxy_cmd)); + proxy_cmd.verb = proxy_verb; + return cmd_proxychat(service, session, &proxy_cmd); + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cmd->argc == 0) { + hybbx_chat_list_channels(session, chat); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "show")) { + if (cmd->argc != 1) { + hybbx_session_write_line(session, "Usage: /chat show"); + return HYBBX_OK; + } + hybbx_chat_show_channel(service, session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "showall")) { + if (cmd->argc != 1) { + hybbx_session_write_line(session, "Usage: /chat showall"); + return HYBBX_OK; + } + hybbx_chat_show_all(service, session); + return HYBBX_OK; + } + + rc = hybbx_chat_resolve_channel(chat, cmd->argv[0], &channel_index); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown chat channel."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + current = hybbx_session_chat_channel(session); + if (current == channel_index) { + snprintf(line, sizeof(line), "Already in channel %u %s.", + channel_index, + hybbx_chat_channel_name(chat, channel_index)); + hybbx_session_write_line(session, line); + return HYBBX_OK; + } + + rc = hybbx_session_join_chat_channel(session, channel_index); + if (rc != HYBBX_OK) { + return rc; + } + + name = hybbx_chat_channel_name(chat, channel_index); + snprintf(line, sizeof(line), "Channel %u: %s", channel_index, name); + hybbx_session_write_line(session, line); + hybbx_session_write_line(session, + "Each line is a message; /leave or /main to exit."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_conference(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char topic[HYBBX_CONFERENCE_TOPIC_MAX]; + const char *user; + size_t i; + size_t pos = 0; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use conference."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc < 2) { + hybbx_session_write_line(session, + "Usage: /conference "); + return HYBBX_OK; + } + + user = cmd->argv[cmd->argc - 1]; + topic[0] = '\0'; + + for (i = 0; i + 1 < cmd->argc; i++) { + size_t part_len = strlen(cmd->argv[i]); + + if (i > 0) { + if (pos + 1 >= sizeof(topic)) { + hybbx_session_write_line(session, "Topic too long."); + return HYBBX_OK; + } + topic[pos++] = ' '; + topic[pos] = '\0'; + } + + if (pos + part_len >= sizeof(topic)) { + hybbx_session_write_line(session, "Topic too long."); + return HYBBX_OK; + } + + memcpy(topic + pos, cmd->argv[i], part_len); + pos += part_len; + topic[pos] = '\0'; + } + + return hybbx_conference_start(service, session, topic, user); +} + +static void mail_join_subject(const hybbx_parsed_command_t *cmd, + char *out, size_t out_len) +{ + size_t i; + size_t pos = 0; + + if (out == NULL || out_len == 0) { + return; + } + + out[0] = '\0'; + if (cmd == NULL || cmd->argc < 3) { + return; + } + + for (i = 2; i < cmd->argc; i++) { + size_t part_len; + + if (cmd->argv[i] == NULL) { + continue; + } + + if (pos > 0) { + if (pos + 1 >= out_len) { + break; + } + out[pos++] = ' '; + out[pos] = '\0'; + } + + part_len = strlen(cmd->argv[i]); + if (pos + part_len >= out_len) { + part_len = out_len - pos - 1; + } + memcpy(out + pos, cmd->argv[i], part_len); + pos += part_len; + out[pos] = '\0'; + } +} + +static hybbx_result_t cmd_proxymail(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use proxymail."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc == 0) { + (void)hybbx_session_enter_proxymail(session); + hybbx_proxymail_list_inbox(service, session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "list")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc >= 2) { + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /proxymail list [-]"); + return HYBBX_OK; + } + } + + (void)hybbx_session_enter_proxymail(session); + hybbx_proxymail_list_inbox_range(service, session, from, to); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "delete") || str_ieq(cmd->argv[0], "del")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, + "Usage: /proxymail delete "); + return HYBBX_OK; + } + + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /proxymail delete "); + return HYBBX_OK; + } + + (void)hybbx_session_enter_proxymail(session); + return hybbx_proxymail_delete_range(service, session, from, to); + } + + if (str_ieq(cmd->argv[0], "recycle")) { + (void)hybbx_session_enter_proxymail(session); + return hybbx_proxymail_recycle_empty(service, session); + } + + if (str_ieq(cmd->argv[0], "read")) { + unsigned index; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, "Usage: /proxymail read "); + return HYBBX_OK; + } + + index = (unsigned)strtoul(cmd->argv[1], NULL, 10); + if (index == 0) { + hybbx_session_write_line(session, "Usage: /proxymail read "); + return HYBBX_OK; + } + + (void)hybbx_session_enter_proxymail(session); + return hybbx_proxymail_read(service, session, index); + } + + if (str_ieq(cmd->argv[0], "send")) { + if (cmd->argc < 3) { + hybbx_session_write_line(session, + "Usage: /proxymail send @
"); + return HYBBX_OK; + } + + mail_join_subject(cmd, subject, sizeof(subject)); + if (subject[0] == '\0') { + hybbx_session_write_line(session, + "Usage: /proxymail send @
"); + return HYBBX_OK; + } + + rc = hybbx_session_proxymail_compose_start(session, cmd->argv[1], + subject); + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, + "Invalid address or subject. Use user@remote-main."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, + "Compose body. /proxymail done to send."); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "done")) { + const char *body; + const char *to_address; + const char *mail_subject; + + if (!hybbx_session_proxymail_composing(session)) { + hybbx_session_write_line(session, "Not composing proxymail."); + return HYBBX_OK; + } + + to_address = hybbx_session_proxymail_compose_to(session); + mail_subject = hybbx_session_proxymail_compose_subject(session); + body = hybbx_session_proxymail_compose_body(session); + + rc = hybbx_proxymail_deliver(service, hybbx_session_display_name(session), + to_address, mail_subject, body); + + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, "Invalid address — use user@mainname."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, + "Proxymail is not available — enable mains_proxy and peer links."); + hybbx_session_proxymail_compose_cancel(session); + hybbx_session_leave_area(session); + hybbx_session_show_prompt(session); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, + "Unknown remote main — check user@mainname and peer_id."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Send failed."); + return rc; + } + + hybbx_session_write_line(session, "Proxymail sent."); + hybbx_session_proxymail_compose_cancel(session); + hybbx_session_leave_area(session); + hybbx_session_show_prompt(session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "cancel")) { + if (hybbx_session_proxymail_composing(session)) { + hybbx_session_proxymail_compose_cancel(session); + hybbx_session_leave_area(session); + hybbx_session_write_line(session, "Compose cancelled."); + } else { + hybbx_session_write_line(session, "Not composing proxymail."); + } + return HYBBX_OK; + } + + hybbx_session_write_line(session, + "Unknown /proxymail subcommand. Try /help proxymail."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_proxychat(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + hybbx_result_t rc; + + (void)service; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use proxychat."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc > 0) { + hybbx_session_write_line(session, + "Usage: /proxychat — chat with users on other mains."); + return HYBBX_OK; + } + + rc = hybbx_session_enter_proxychat(session); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_proxychat_show_banner(session); + return HYBBX_OK; +} + +static hybbx_result_t cmd_mail(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + if (cmd->argc > 0 && str_ieq(cmd->argv[0], "proxymail")) { + hybbx_parsed_command_t proxy_cmd; + char proxy_verb[] = "proxymail"; + unsigned i; + + memset(&proxy_cmd, 0, sizeof(proxy_cmd)); + proxy_cmd.verb = proxy_verb; + proxy_cmd.argc = cmd->argc > 0 ? cmd->argc - 1 : 0; + for (i = 0; i < proxy_cmd.argc && i < HYBBX_CMD_TOKEN_MAX; i++) { + proxy_cmd.argv[i] = cmd->argv[i + 1]; + } + return cmd_proxymail(service, session, &proxy_cmd); + } + + if (cmd->argc == 0) { + hybbx_mail_list_inbox(service, session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "list")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc >= 2) { + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /mail list [-] e.g. list 1-15 list 5-20"); + return HYBBX_OK; + } + } + + hybbx_mail_list_inbox_range(service, session, from, to); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "delete") || str_ieq(cmd->argv[0], "del")) { + unsigned from = 1; + unsigned to = 0; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, + "Usage: /mail delete e.g. delete 3 delete 5-20"); + return HYBBX_OK; + } + + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /mail delete e.g. delete 3 delete 5-20"); + return HYBBX_OK; + } + + return hybbx_mail_delete_range(service, session, from, to); + } + + if (str_ieq(cmd->argv[0], "recycle")) { + return hybbx_mail_recycle_empty(service, session); + } + + if (str_ieq(cmd->argv[0], "read")) { + unsigned index; + + if (cmd->argc < 2 || cmd->argv[1] == NULL) { + hybbx_session_write_line(session, "Usage: /mail read "); + return HYBBX_OK; + } + + index = (unsigned)strtoul(cmd->argv[1], NULL, 10); + if (index == 0) { + hybbx_session_write_line(session, "Usage: /mail read "); + return HYBBX_OK; + } + + return hybbx_mail_read(service, session, index); + } + + if (str_ieq(cmd->argv[0], "send")) { + if (cmd->argc < 3) { + hybbx_session_write_line(session, + "Usage: /mail send "); + return HYBBX_OK; + } + + mail_join_subject(cmd, subject, sizeof(subject)); + if (subject[0] == '\0') { + hybbx_session_write_line(session, + "Usage: /mail send "); + return HYBBX_OK; + } + + rc = hybbx_session_mail_compose_start(session, cmd->argv[1], subject); + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, "Subject too long."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_session_write_line(session, "Compose body. /mail done to send."); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "done")) { + const char *body; + const char *to_user; + const char *mail_subject; + + if (!hybbx_session_mail_composing(session)) { + hybbx_session_write_line(session, "Not composing mail."); + return HYBBX_OK; + } + + to_user = hybbx_session_mail_compose_to(session); + mail_subject = hybbx_session_mail_compose_subject(session); + body = hybbx_session_mail_compose_body(session); + + rc = hybbx_mail_deliver(service, hybbx_session_display_name(session), + to_user, mail_subject, body); + + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "Unknown recipient."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_DENIED) { + hybbx_session_write_line(session, "Cannot mail that user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Send failed."); + return rc; + } + + hybbx_session_write_line(session, "Message sent."); + hybbx_session_leave_area(session); + hybbx_session_show_prompt(session); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "cancel")) { + if (hybbx_session_mail_composing(session)) { + hybbx_session_leave_area(session); + hybbx_session_write_line(session, "Compose cancelled."); + } else { + hybbx_session_write_line(session, "Not composing mail."); + } + return HYBBX_OK; + } + + hybbx_session_write_line(session, "Unknown /mail subcommand. Try /help mail."); + return HYBBX_OK; +} + +static void cmd_emit_current_area(hybbx_session_t *session) +{ + const char *name = hybbx_session_area_name(hybbx_session_area(session)); + char line[64]; + + snprintf(line, sizeof(line), "%c%s.", + (char)toupper((unsigned char)name[0]), name + 1); + hybbx_session_write_line(session, line); + hybbx_session_show_prompt(session); +} + +static hybbx_result_t cmd_leave(hybbx_session_t *session) +{ + hybbx_session_area_t before = hybbx_session_area(session); + + hybbx_session_leave_area(session); + if (hybbx_session_area(session) != before) { + cmd_emit_current_area(session); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_main(hybbx_session_t *session) +{ + hybbx_session_area_t before = hybbx_session_area(session); + + hybbx_session_go_main(session); + if (before != HYBBX_AREA_MAIN) { + cmd_emit_current_area(session); + } + + return HYBBX_OK; +} + +static hybbx_result_t cmd_exit(hybbx_session_t *session) +{ + hybbx_session_write_line(session, "Goodbye."); + return HYBBX_SESSION_END; +} + +static hybbx_result_t cmd_broadcast(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char message[HYBBX_BROADCAST_MESSAGE_MAX + 1]; + size_t off = 0; + int i; + hybbx_result_t rc; + + if (cmd->argc < 1) { + cmd_registry_usage(session, "broadcast"); + return HYBBX_OK; + } + + if (str_ieq(cmd->argv[0], "ax25")) { + if (cmd->argc > 1) { + hybbx_session_write_line(session, + "RF beacon uses ax25_auto_message from INI. Use: /broadcast ax25"); + return HYBBX_OK; + } + + rc = hybbx_broadcast_ax25_manual(service); + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, "AX.25 broadcast is disabled."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_DENIED) { + hybbx_session_write_line(session, + "No qualifying packet-radio links for AX.25 broadcast."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_BUSY) { + hybbx_session_write_line(session, + "AX.25 broadcast deferred (link busy)."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "AX.25 broadcast failed."); + return HYBBX_ERR_IO; + } + + hybbx_session_write_line(session, "AX.25 broadcast sent."); + return HYBBX_OK; + } + + message[0] = '\0'; + for (i = 0; i < (int)cmd->argc; i++) { + size_t part_len = strlen(cmd->argv[i]); + + if (off > 0 && off < sizeof(message) - 1) { + message[off++] = ' '; + } + if (off + part_len >= sizeof(message)) { + part_len = sizeof(message) - off - 1; + } + if (part_len > 0) { + memcpy(message + off, cmd->argv[i], part_len); + off += part_len; + } + } + message[off] = '\0'; + + if (message[0] == '\0') { + hybbx_session_write_line(session, "Missing announce message."); + return HYBBX_ERR_INVALID; + } + + rc = hybbx_broadcast_announce(service, session, message); + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, "Broadcast is disabled."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_INVALID) { + hybbx_session_write_line(session, "Message too long."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Broadcast failed."); + return HYBBX_ERR_IO; + } + + hybbx_session_write_line(session, "Broadcast sent."); + return HYBBX_OK; +} + +static hybbx_result_t cmd_shutdown(hybbx_service_t *service, + hybbx_session_t *session) +{ + if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + hybbx_security_log_write("shutdown ip=%s user=%s transport=%s", + hybbx_session_record(session) != NULL && + hybbx_session_record(session)->remote[0] != '\0' ? + hybbx_session_record(session)->remote : "?", + hybbx_session_display_name(session), + hybbx_session_record(session) != NULL ? + hybbx_session_record(session)->transport : "?"); + hybbx_session_write_line(session, "Shutting down HyBBX."); + hybbx_service_request_shutdown(service, 0); + return HYBBX_OK; +} + +static hybbx_result_t cmd_restart(hybbx_service_t *service, + hybbx_session_t *session) +{ + if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + cmd_deny_privilege(session); + return HYBBX_ERR_DENIED; + } + + hybbx_security_log_write("restart ip=%s user=%s transport=%s", + hybbx_session_record(session) != NULL && + hybbx_session_record(session)->remote[0] != '\0' ? + hybbx_session_record(session)->remote : "?", + hybbx_session_display_name(session), + hybbx_session_record(session) != NULL ? + hybbx_session_record(session)->transport : "?"); + hybbx_session_write_line(session, "Restarting HyBBX."); + hybbx_service_request_shutdown(service, 1); + return HYBBX_OK; +} + +static hybbx_result_t cmd_version(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_texts_config_t *texts = hybbx_service_get_texts(service); + + if (texts == NULL) { + return HYBBX_ERR_INVALID; + } + + return hybbx_texts_send_version(texts, session); +} + +static hybbx_result_t cmd_clear(hybbx_session_t *session) +{ + return hybbx_session_clear_terminal(session); +} + +static hybbx_result_t cmd_echo(hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + char buf[48]; + + if (cmd->argc < 2) { + snprintf(buf, sizeof(buf), "Input echo is %s.", + hybbx_bool_to_string(hybbx_session_input_echo(session))); + hybbx_session_write_line(session, buf); + return HYBBX_OK; + } + + if (hybbx_bool_is_true(cmd->argv[1])) { + return hybbx_session_set_input_echo(session, 1); + } + + if (hybbx_bool_is_false(cmd->argv[1])) { + return hybbx_session_set_input_echo(session, 0); + } + + hybbx_session_write_line(session, "Usage: /echo yes|no"); + return HYBBX_ERR_INVALID; +} + +typedef struct monitor_list_ctx { + hybbx_session_t *requester; + unsigned count; +} monitor_list_ctx_t; + +static void monitor_list_visitor(hybbx_session_t *session, void *userdata) +{ + monitor_list_ctx_t *ctx = (monitor_list_ctx_t *)userdata; + const hybbx_session_record_t *rec; + const char *user; + const char *plugin; + char line[96]; + + if (ctx == NULL || session == NULL || !hybbx_session_logged_in(session)) { + return; + } + + if (!hybbx_session_is_interactive_user(session)) { + return; + } + + if (hybbx_session_hidden_from_who(session)) { + return; + } + + user = hybbx_session_display_name(session); + rec = hybbx_session_record(session); + plugin = (rec != NULL && rec->transport[0] != '\0') + ? rec->transport + : (session->transport != NULL ? session->transport->name : "?"); + snprintf(line, sizeof(line), " %s@%s", user, plugin); + hybbx_session_write_line(ctx->requester, line); + ctx->count++; +} + +static void monitor_show_cmds(hybbx_session_t *session) +{ + hybbx_session_write_line(session, "Monitor admin commands:"); + hybbx_session_write_line(session, + " /monitor meet [force] — conference invite (20s) or force"); + hybbx_session_write_line(session, + "Run any of these as normal /verb (stay invisible while Sysop hidden):"); + hybbx_commands_registry_show_sysop_cmds(session); +} + +static hybbx_result_t cmd_monitor(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + const char *sub; + monitor_list_ctx_t list_ctx; + const hybbx_monitor_config_t *mcfg; + char line[128]; + + if (!hybbx_monitor_enabled()) { + hybbx_session_write_line(session, "Monitor is disabled ([monitor] enabled=no)."); + return HYBBX_ERR_DENIED; + } + + if (!hybbx_monitor_session_may_use(session) && + !hybbx_commands_registry_verb_allowed( + hybbx_session_user_level(session), "monitor")) { + hybbx_session_write_line(session, "Monitor access denied."); + return HYBBX_ERR_DENIED; + } + + sub = (cmd->argc >= 1) ? cmd->argv[0] : "list"; + + if (str_ieq(sub, "on")) { + return hybbx_monitor_set_active(session, 1); + } + + if (str_ieq(sub, "off")) { + return hybbx_monitor_set_active(session, 0); + } + + if (str_ieq(sub, "cmd") || str_ieq(sub, "cmds") || str_ieq(sub, "commands")) { + monitor_show_cmds(session); + return HYBBX_OK; + } + + if (str_ieq(sub, "meet")) { + int force = 0; + const char *user; + + if (cmd->argc < 2) { + hybbx_session_write_line(session, + "Usage: /monitor meet [force]"); + return HYBBX_ERR_INVALID; + } + user = cmd->argv[1]; + if (cmd->argc >= 3 && str_ieq(cmd->argv[2], "force")) { + force = 1; + } + if (!hybbx_monitor_is_active(session)) { + hybbx_session_write_line(session, + "Turn monitor on first: /monitor on"); + return HYBBX_ERR_DENIED; + } + return hybbx_conference_monitor_meet(service, session, user, force); + } + + if (!str_ieq(sub, "list") && cmd->argc >= 1) { + hybbx_session_write_line(session, + "Usage: /monitor on|off|list|cmds|meet [force]"); + return HYBBX_ERR_INVALID; + } + + mcfg = hybbx_monitor_config_get(); + snprintf(line, sizeof(line), "Monitor: %s", + hybbx_monitor_is_active(session) ? "on" : "off"); + hybbx_session_write_line(session, line); + if (mcfg != NULL) { + snprintf(line, sizeof(line), + "Follow: hybbx=%s security=%s invisible-sysop=%s " + "invite_timeout=%us", + mcfg->follow_hybbx ? "yes" : "no", + mcfg->follow_security ? "yes" : "no", + mcfg->invisible_sysop ? "yes" : "no", + mcfg->invite_timeout_sec); + hybbx_session_write_line(session, line); + } + if (hybbx_session_hidden_from_who(session)) { + hybbx_session_write_line(session, "You are hidden from /who."); + } + hybbx_session_write_line(session, "Online (visible):"); + list_ctx.requester = session; + list_ctx.count = 0; + hybbx_service_visit_sessions(service, monitor_list_visitor, &list_ctx); + if (list_ctx.count == 0) { + hybbx_session_write_line(session, " (none)"); + } + return HYBBX_OK; +} + +hybbx_result_t hybbx_command_dispatch(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_parsed_command_t *cmd) +{ + if (cmd == NULL || service == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cmd->scope != HYBBX_CMD_SCOPE_HYBBX) { + return HYBBX_ERR_INVALID; + } + + (void)hybbx_session_command_gap(session); + + if (cmd->verb == NULL || cmd->verb[0] == '\0') { + return cmd_help(session, cmd); + } + + if (str_ieq(cmd->verb, "help") || str_ieq(cmd->verb, "?")) { + return cmd_help(session, cmd); + } + + { + hybbx_result_t access = cmd_check_access(session, cmd->verb); + if (access != HYBBX_OK) { + return access; + } + } + + if (str_ieq(cmd->verb, "news")) { + return cmd_news(service, session); + } + + if (str_ieq(cmd->verb, "banner") || str_ieq(cmd->verb, "loginmsg")) { + return cmd_banner(service, session); + } + + if (str_ieq(cmd->verb, "motd")) { + return cmd_motd(service, session); + } + + if (str_ieq(cmd->verb, "rules") || str_ieq(cmd->verb, "legal")) { + return cmd_rules(service, session); + } + + if (str_ieq(cmd->verb, "who") || str_ieq(cmd->verb, "online")) { + return cmd_who(service, session); + } + + if (str_ieq(cmd->verb, "monitor") || str_ieq(cmd->verb, "mon")) { + return cmd_monitor(service, session, cmd); + } + + if (str_ieq(cmd->verb, "users")) { + return cmd_users(service, session); + } + + if (str_ieq(cmd->verb, "session") || str_ieq(cmd->verb, "info")) { + return cmd_session(session); + } + + if (str_ieq(cmd->verb, "version") || str_ieq(cmd->verb, "ver")) { + return cmd_version(service, session); + } + + if (str_ieq(cmd->verb, "leave") || str_ieq(cmd->verb, "back")) { + return cmd_leave(session); + } + + if (str_ieq(cmd->verb, "main")) { + return cmd_main(session); + } + + if (str_ieq(cmd->verb, "menu")) { + if (cmd->argc == 0) { + hybbx_commands_registry_show_menu(session); + return HYBBX_OK; + } + return cmd_help_topic(session, cmd->argv[0]); + } + + if (str_ieq(cmd->verb, "index")) { + if (cmd->argc == 0) { + hybbx_commands_registry_show_index(session); + return HYBBX_OK; + } + return cmd_help_topic(session, cmd->argv[0]); + } + + if (str_ieq(cmd->verb, "alias")) { + if (cmd->argc == 0) { + hybbx_commands_registry_show_aliases(session); + return HYBBX_OK; + } + return cmd_help_topic(session, cmd->argv[0]); + } + + if (str_ieq(cmd->verb, "clear") || str_ieq(cmd->verb, "cls") || + str_ieq(cmd->verb, "reset")) { + return cmd_clear(session); + } + + if (str_ieq(cmd->verb, "echo")) { + return cmd_echo(session, cmd); + } + + if (str_ieq(cmd->verb, "chat")) { + return cmd_chat(service, session, cmd); + } + + if (str_ieq(cmd->verb, "conference") || str_ieq(cmd->verb, "meeting")) { + return cmd_conference(service, session, cmd); + } + + if (str_ieq(cmd->verb, "mail")) { + return cmd_mail(service, session, cmd); + } + + if (str_ieq(cmd->verb, "proxymail")) { + return cmd_proxymail(service, session, cmd); + } + + if (str_ieq(cmd->verb, "proxychat")) { + return cmd_proxychat(service, session, cmd); + } + + if (str_ieq(cmd->verb, "login")) { + return cmd_login(service, session, cmd); + } + + if (str_ieq(cmd->verb, "register")) { + return cmd_register(service, session, cmd); + } + + if (str_ieq(cmd->verb, "changeme")) { + return cmd_changeme(service, session, cmd); + } + + if (str_ieq(cmd->verb, "changeuser") || str_ieq(cmd->verb, "userchange")) { + return cmd_userchange(service, session, cmd); + } + + if (str_ieq(cmd->verb, "deleteuser") || str_ieq(cmd->verb, "userdelete")) { + return cmd_userdelete(service, session, cmd); + } + + if (str_ieq(cmd->verb, "usercreate") || str_ieq(cmd->verb, "createuser")) { + return cmd_createuser(service, session, cmd); + } + + if (str_ieq(cmd->verb, "activate")) { + return cmd_activate(service, session, cmd); + } + + if (str_ieq(cmd->verb, "promote")) { + return cmd_promote(service, session, cmd); + } + + if (str_ieq(cmd->verb, "demote")) { + return cmd_demote(service, session, cmd); + } + + if (str_ieq(cmd->verb, "delete") || str_ieq(cmd->verb, "del")) { + return cmd_delete(service, session, cmd); + } + + if (str_ieq(cmd->verb, "deleteme")) { + return cmd_deleteme(service, session, cmd); + } + + if (str_ieq(cmd->verb, "broadcast") || str_ieq(cmd->verb, "announce")) { + return cmd_broadcast(service, session, cmd); + } + + if (str_ieq(cmd->verb, "shutdown")) { + return cmd_shutdown(service, session); + } + + if (str_ieq(cmd->verb, "restart")) { + return cmd_restart(service, session); + } + + if (str_ieq(cmd->verb, "exit") || str_ieq(cmd->verb, "logout") || + str_ieq(cmd->verb, "bye") || str_ieq(cmd->verb, "quit")) { + return cmd_exit(session); + } + + hybbx_session_write_line(session, cmd_help_unknown(session)); + return HYBBX_ERR_NOT_FOUND; +} diff --git a/src/core/command_parse.c b/src/core/command_parse.c new file mode 100644 index 0000000..f96bf6c --- /dev/null +++ b/src/core/command_parse.c @@ -0,0 +1,347 @@ +#include "hybbx/command.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include +#include +#include + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + if (!hybbx_size_ok(len)) { + return NULL; + } + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static const char *skip_leading_space(const char *line) +{ + if (line == NULL) { + return NULL; + } + + while (*line != '\0' && isspace((unsigned char)*line)) { + line++; + } + + return line; +} + +static char *ltrim_copy(char *s) +{ + char *start; + + if (s == NULL) { + return NULL; + } + + start = (char *)skip_leading_space(s); + if (start != s) { + memmove(s, start, strlen(start) + 1); + } + + return s; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int strip_one_command_alias(char *rest, const char *alias) +{ + size_t alias_len; + + if (alias == NULL || rest == NULL) { + return -1; + } + + alias_len = strlen(alias); + + if (str_ieq(rest, alias)) { + return 1; + } + + if (strncmp(rest, alias, alias_len) == 0 && + isspace((unsigned char)rest[alias_len])) { + memmove(rest, rest + alias_len, strlen(rest + alias_len) + 1); + ltrim_copy(rest); + return rest[0] == '\0' ? 1 : 0; + } + + return -1; +} + +static int strip_command_alias(char *rest) +{ + static const char *aliases[] = { "commands", "command", "cmd", NULL }; + int rc; + size_t i; + + if (rest == NULL) { + return 0; + } + + if (rest[0] == '\0') { + return 1; + } + + for (i = 0; aliases[i] != NULL; i++) { + rc = strip_one_command_alias(rest, aliases[i]); + if (rc >= 0) { + return rc; + } + } + + return 0; +} + +hybbx_command_scope_t hybbx_command_classify(const char *line) +{ + const char *p = skip_leading_space(line); + + if (p == NULL || *p == '\0') { + return HYBBX_CMD_SCOPE_LOCAL; + } + + if (*p == '/') { + return HYBBX_CMD_SCOPE_HYBBX; + } + + if (*p == ';' || *p == '#') { + return HYBBX_CMD_SCOPE_COMMENT; + } + + return HYBBX_CMD_SCOPE_LOCAL; +} + +int hybbx_command_is_comment(const char *line) +{ + return hybbx_command_classify(line) == HYBBX_CMD_SCOPE_COMMENT; +} + +int hybbx_command_is_hybbx(const char *line) +{ + return hybbx_command_classify(line) == HYBBX_CMD_SCOPE_HYBBX; +} + +static size_t count_tokens(const char *line) +{ + size_t count = 0; + size_t i = 0; + int in_token = 0; + + while (line[i] != '\0') { + if (!isspace((unsigned char)line[i])) { + if (!in_token) { + count++; + in_token = 1; + } + } else { + in_token = 0; + } + i++; + } + + return count; +} + +static hybbx_result_t split_tokens(char *line, char **tokens, size_t max) +{ + size_t count = 0; + size_t i = 0; + + while (line[i] != '\0' && count < max) { + while (line[i] != '\0' && isspace((unsigned char)line[i])) { + i++; + } + if (line[i] == '\0') { + break; + } + tokens[count++] = line + i; + while (line[i] != '\0' && !isspace((unsigned char)line[i])) { + i++; + } + if (line[i] != '\0') { + line[i] = '\0'; + i++; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_hybbx_line(const char *line, hybbx_parsed_command_t *out) +{ + char *rest; + char *work; + size_t token_count; + char **tokens; + size_t i; + hybbx_result_t rc; + + if (line[0] != '/') { + return HYBBX_ERR_INVALID; + } + + rest = hybbx_strdup(line + 1); + if (rest == NULL) { + return HYBBX_ERR_NOMEM; + } + + ltrim_copy(rest); + + if (strip_command_alias(rest)) { + out->verb = hybbx_strdup("help"); + if (out->verb == NULL) { + free(rest); + return HYBBX_ERR_NOMEM; + } + free(rest); + return HYBBX_OK; + } + + token_count = count_tokens(rest); + if (token_count == 0) { + out->verb = hybbx_strdup("help"); + free(rest); + if (out->verb == NULL) { + return HYBBX_ERR_NOMEM; + } + return HYBBX_OK; + } + + if (token_count > HYBBX_CMD_TOKEN_MAX) { + free(rest); + return HYBBX_ERR_INVALID; + } + + work = hybbx_strdup(rest); + free(rest); + if (work == NULL) { + return HYBBX_ERR_NOMEM; + } + + tokens = calloc(token_count, sizeof(*tokens)); + if (tokens == NULL) { + free(work); + return HYBBX_ERR_NOMEM; + } + + rc = split_tokens(work, tokens, token_count); + if (rc != HYBBX_OK) { + free(tokens); + free(work); + return rc; + } + + out->verb = hybbx_strdup(tokens[0]); + if (out->verb == NULL) { + free(tokens); + free(work); + return HYBBX_ERR_NOMEM; + } + + out->argc = token_count > 1 ? token_count - 1 : 0; + if (out->argc > 0) { + out->argv = calloc(out->argc, sizeof(*out->argv)); + if (out->argv == NULL) { + free(tokens); + free(work); + hybbx_command_free(out); + return HYBBX_ERR_NOMEM; + } + + for (i = 0; i < out->argc; i++) { + out->argv[i] = hybbx_strdup(tokens[i + 1]); + if (out->argv[i] == NULL) { + free(tokens); + free(work); + hybbx_command_free(out); + return HYBBX_ERR_NOMEM; + } + } + } + + free(tokens); + free(work); + return HYBBX_OK; +} + +hybbx_result_t hybbx_command_parse(const char *line, + hybbx_parsed_command_t *out) +{ + char *trimmed; + hybbx_result_t rc; + + if (line == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + trimmed = hybbx_strdup(skip_leading_space(line)); + if (trimmed == NULL) { + return HYBBX_ERR_NOMEM; + } + + out->scope = hybbx_command_classify(trimmed); + out->line = trimmed; + + if (out->scope != HYBBX_CMD_SCOPE_HYBBX) { + return HYBBX_OK; + } + + rc = parse_hybbx_line(trimmed, out); + if (rc != HYBBX_OK) { + hybbx_command_free(out); + } + + return rc; +} + +void hybbx_command_free(hybbx_parsed_command_t *cmd) +{ + size_t i; + + if (cmd == NULL) { + return; + } + + free(cmd->line); + free(cmd->verb); + + for (i = 0; i < cmd->argc; i++) { + free(cmd->argv[i]); + } + free(cmd->argv); + + memset(cmd, 0, sizeof(*cmd)); +} diff --git a/src/core/commands_registry.c b/src/core/commands_registry.c new file mode 100644 index 0000000..6f63178 --- /dev/null +++ b/src/core/commands_registry.c @@ -0,0 +1,1429 @@ +#include "hybbx/commands_registry.h" +#include "hybbx/session.h" +#include "hybbx/monitor.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include + +typedef struct menu_subarea { + char verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned verb_count; +} menu_subarea_t; + +typedef struct menu_area { + char label[16]; + char verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned verb_count; + menu_subarea_t subareas[HYBBX_AREAS_SUB_MAX]; + unsigned subarea_count; +} menu_area_t; + +typedef struct alias_entry { + char canonical[HYBBX_CMD_VERB_MAX]; + char aliases[HYBBX_COMMANDS_ALIAS_PER][HYBBX_CMD_VERB_MAX]; + unsigned alias_count; +} alias_entry_t; + +typedef struct target_right_rule { + hybbx_user_level_t actor; + hybbx_user_level_t targets[5]; + unsigned target_count; +} target_right_rule_t; + +typedef struct promote_right_rule { + hybbx_user_level_t actor; + hybbx_user_level_t to_level; + hybbx_user_level_t from_levels[4]; + unsigned from_count; +} promote_right_rule_t; + +typedef struct demote_right_rule { + hybbx_user_level_t actor; + hybbx_user_level_t from_levels[4]; + unsigned from_count; +} demote_right_rule_t; + +typedef struct commands_registry { + int loaded; + char menu_header[HYBBX_COMMANDS_HEADER_MAX]; + char index_header[HYBBX_COMMANDS_HEADER_MAX]; + char alias_header[HYBBX_COMMANDS_HEADER_MAX]; + menu_area_t areas[HYBBX_AREAS_MAX]; + unsigned area_count; + char menu_levels[HYBBX_MENU_LEVELS_MAX][16]; + char menu_area_labels[HYBBX_MENU_LEVELS_MAX] + [HYBBX_MENU_AREAS_PER_LEVEL][16]; + unsigned menu_area_count[HYBBX_MENU_LEVELS_MAX]; + unsigned menu_level_count; + char index_labels[HYBBX_MENU_AREAS_PER_LEVEL][16]; + unsigned index_label_count; + target_right_rule_t userchange_rules[HYBBX_RIGHTS_TARGET_RULES_MAX]; + unsigned userchange_rule_count; + target_right_rule_t userdelete_rules[HYBBX_RIGHTS_TARGET_RULES_MAX]; + unsigned userdelete_rule_count; + target_right_rule_t delete_rules[HYBBX_RIGHTS_TARGET_RULES_MAX]; + unsigned delete_rule_count; + promote_right_rule_t promote_rules[HYBBX_RIGHTS_PROMOTE_RULES_MAX]; + unsigned promote_rule_count; + demote_right_rule_t demote_rules[HYBBX_RIGHTS_DEMOTE_RULES_MAX]; + unsigned demote_rule_count; + alias_entry_t aliases[HYBBX_COMMANDS_ALIASES_MAX]; + unsigned alias_count; + hybbx_command_def_t commands[HYBBX_COMMANDS_MAX]; + unsigned command_count; + char alias_lines[HYBBX_COMMANDS_ALIAS_LINES][HYBBX_LINE_MAX]; + unsigned alias_line_count; +} commands_registry_t; + +static commands_registry_t g_registry; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void trim_inplace(char *s) +{ + char *start = s; + char *end; + + if (s == NULL) { + return; + } + + while (*start != '\0' && isspace((unsigned char)*start)) { + start++; + } + + if (start != s) { + memmove(s, start, strlen(start) + 1); + } + + end = s + strlen(s); + while (end > s && isspace((unsigned char)end[-1])) { + end--; + } + *end = '\0'; +} + +static void strip_quotes(char *s) +{ + size_t len; + + if (s == NULL) { + return; + } + + trim_inplace(s); + len = strlen(s); + if (len >= 2 && + ((s[0] == '"' && s[len - 1] == '"') || + (s[0] == '\'' && s[len - 1] == '\''))) { + s[len - 1] = '\0'; + memmove(s, s + 1, len - 1); + } +} + +static unsigned line_indent(const char *line) +{ + unsigned n = 0; + + while (line[n] == ' ') { + n++; + } + + return n; +} + +static const char *line_key(const char *line) +{ + if (line == NULL) { + return ""; + } + + while (*line == ' ') { + line++; + } + + return line; +} + +static int parse_bracket_list(const char *value, char out[][HYBBX_CMD_VERB_MAX], + unsigned max, unsigned *count) +{ + const char *p; + char token[HYBBX_CMD_VERB_MAX]; + size_t tlen; + + if (value == NULL || count == NULL) { + return 0; + } + + *count = 0; + p = strchr(value, '['); + if (p == NULL) { + return 0; + } + p++; + + while (*p != '\0' && *p != ']') { + while (*p == ' ' || *p == ',') { + p++; + } + if (*p == ']' || *p == '\0') { + break; + } + + tlen = 0; + while (*p != '\0' && *p != ',' && *p != ']' && + !isspace((unsigned char)*p) && tlen + 1 < sizeof(token)) { + token[tlen++] = *p++; + } + token[tlen] = '\0'; + if (token[0] == '\0') { + continue; + } + + if (*count >= max) { + return -1; + } + + hybbx_strlcpy(out[*count], token, HYBBX_CMD_VERB_MAX); + (*count)++; + } + + return 0; +} + +static int parse_level_list(const char *value, hybbx_user_level_t *out, + unsigned max, unsigned *count) +{ + char tokens[8][HYBBX_CMD_VERB_MAX]; + unsigned n = 0; + unsigned i; + + if (value == NULL || count == NULL) { + return 0; + } + + if (parse_bracket_list(value, tokens, max, &n) != 0) { + return -1; + } + + *count = 0; + for (i = 0; i < n && *count < max; i++) { + out[*count] = hybbx_user_level_parse(tokens[i]); + (*count)++; + } + + return 0; +} + +static hybbx_user_level_t level_from_name(const char *name) +{ + return hybbx_user_level_parse(name); +} + +static const char *menu_level_name(hybbx_user_level_t level) +{ + switch (level) { + case HYBBX_LEVEL_SYSOP: + return "Sysop"; + case HYBBX_LEVEL_ADMIN: + return "Admin"; + case HYBBX_LEVEL_MOD: + return "Mod"; + case HYBBX_LEVEL_USER: + return "User"; + default: + return "Guest"; + } +} + +static const menu_area_t *area_find(const char *label) +{ + unsigned i; + + if (label == NULL) { + return NULL; + } + + for (i = 0; i < g_registry.area_count; i++) { + if (str_ieq(g_registry.areas[i].label, label)) { + return &g_registry.areas[i]; + } + } + + return NULL; +} + +static int level_in_list(hybbx_user_level_t level, + const hybbx_user_level_t *list, + unsigned count) +{ + unsigned i; + + for (i = 0; i < count; i++) { + if (list[i] == level) { + return 1; + } + } + + return 0; +} + +static int command_level_allowed(const hybbx_command_def_t *def, + hybbx_user_level_t level) +{ + if (def == NULL) { + return 0; + } + + if (def->only_level != 0 && level != (hybbx_user_level_t)def->only_level) { + return 0; + } + + if (level > def->min_level) { + return 0; + } + + if (def->max_level != 0 && level < def->max_level) { + return 0; + } + + return 1; +} + +/** Non-zero when @p session may see @p verb in menu/index/help (level or grant). */ +static int command_verb_visible(const hybbx_session_t *session, + hybbx_user_level_t level, + const char *verb) +{ + if (hybbx_commands_registry_verb_allowed(level, verb)) { + return 1; + } + + /* [monitor] allow= grants may see /monitor without Sysop min. */ + if (session != NULL && + (str_ieq(verb, "monitor") || str_ieq(verb, "mon")) && + hybbx_monitor_session_may_use(session)) { + return 1; + } + + return 0; +} + +static int target_right_allowed(const target_right_rule_t *rules, + unsigned rule_count, + hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + unsigned i; + + if (hybbx_user_level_is_sysop(target) || + hybbx_user_level_is_guest(target)) { + return 0; + } + + for (i = 0; i < rule_count; i++) { + if (rules[i].actor == actor && + level_in_list(target, rules[i].targets, rules[i].target_count)) { + return 1; + } + } + + return 0; +} + +static hybbx_result_t parse_areas_yaml(commands_registry_t *reg, const char *path) +{ + FILE *fp; + char buf[HYBBX_CONFIG_LINE_MAX]; + enum { + SEC_NONE, + SEC_META, + SEC_AREAS, + SEC_MENU, + SEC_INDEX + } section = SEC_NONE; + menu_area_t *area = NULL; + menu_subarea_t *subarea = NULL; + char cur_menu_level[16]; + + if (reg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + cur_menu_level[0] = '\0'; + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + while (fgets(buf, sizeof(buf), fp) != NULL) { + char *line = buf; + char *colon; + unsigned indent; + + indent = line_indent(line); + trim_inplace(line); + if (line[0] == '\0' || line[0] == '#') { + continue; + } + + if (indent == 0 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + area = NULL; + subarea = NULL; + cur_menu_level[0] = '\0'; + + if (str_ieq(line, "meta")) { + section = SEC_META; + } else if (str_ieq(line, "areas")) { + section = SEC_AREAS; + } else if (str_ieq(line, "menu")) { + section = SEC_MENU; + } else if (str_ieq(line, "index")) { + section = SEC_INDEX; + } else { + section = SEC_NONE; + } + continue; + } + + if (section == SEC_META && indent >= 2) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "menu_header")) { + hybbx_strlcpy(reg->menu_header, colon + 1, + sizeof(reg->menu_header)); + } else if (str_ieq(line_key(line), "index_header")) { + hybbx_strlcpy(reg->index_header, colon + 1, + sizeof(reg->index_header)); + } else if (str_ieq(line_key(line), "alias_header")) { + hybbx_strlcpy(reg->alias_header, colon + 1, + sizeof(reg->alias_header)); + } + continue; + } + + if (section == SEC_AREAS) { + if (indent == 2 && line[0] == '-') { + if (reg->area_count >= HYBBX_AREAS_MAX) { + continue; + } + area = ®->areas[reg->area_count++]; + memset(area, 0, sizeof(*area)); + subarea = NULL; + + line++; + trim_inplace(line); + colon = strchr(line, ':'); + if (colon != NULL) { + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "label")) { + hybbx_strlcpy(area->label, colon + 1, sizeof(area->label)); + } else if (str_ieq(line_key(line), "commands")) { + parse_bracket_list(colon + 1, area->verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP, + &area->verb_count); + } + } + continue; + } + + if (area == NULL) { + continue; + } + + if (indent == 4 && line[0] == '-') { + if (area->subarea_count < HYBBX_AREAS_SUB_MAX) { + subarea = &area->subareas[area->subarea_count++]; + memset(subarea, 0, sizeof(*subarea)); + } + continue; + } + + if (indent >= 4) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "label")) { + hybbx_strlcpy(area->label, colon + 1, sizeof(area->label)); + } else if (str_ieq(line_key(line), "commands")) { + parse_bracket_list(colon + 1, area->verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP, + &area->verb_count); + } + continue; + } + + if (indent >= 6 && subarea != NULL) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "commands")) { + parse_bracket_list(colon + 1, subarea->verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP, + &subarea->verb_count); + } + } + continue; + } + + if (section == SEC_MENU) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + if (reg->menu_level_count < HYBBX_MENU_LEVELS_MAX) { + hybbx_strlcpy(reg->menu_levels[reg->menu_level_count], + line_key(line), + sizeof(reg->menu_levels[0])); + hybbx_strlcpy(cur_menu_level, line_key(line), + sizeof(cur_menu_level)); + reg->menu_level_count++; + } + continue; + } + + if (indent >= 4 && line[0] == '-' && cur_menu_level[0] != '\0') { + unsigned i; + + for (i = 0; i < reg->menu_level_count; i++) { + unsigned n; + + if (!str_ieq(reg->menu_levels[i], cur_menu_level)) { + continue; + } + + n = reg->menu_area_count[i]; + if (n >= HYBBX_MENU_AREAS_PER_LEVEL) { + break; + } + + line++; + trim_inplace(line); + hybbx_strlcpy(reg->menu_area_labels[i][n], line, 16); + reg->menu_area_count[i]++; + break; + } + } + continue; + } + + if (section == SEC_INDEX && indent >= 2 && line[0] == '-') { + if (reg->index_label_count < HYBBX_MENU_AREAS_PER_LEVEL) { + line++; + trim_inplace(line); + hybbx_strlcpy(reg->index_labels[reg->index_label_count], + line, 16); + reg->index_label_count++; + } + } + } + + fclose(fp); + + if (reg->menu_header[0] == '\0' || reg->index_header[0] == '\0' || + reg->area_count == 0) { + return HYBBX_ERR_INVALID; + } + + if (reg->index_label_count == 0) { + unsigned i; + + for (i = 0; i < reg->area_count && + reg->index_label_count < HYBBX_MENU_AREAS_PER_LEVEL; + i++) { + hybbx_strlcpy(reg->index_labels[reg->index_label_count], + reg->areas[i].label, 16); + reg->index_label_count++; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t parse_commands_yaml(commands_registry_t *reg, + const char *path) +{ + FILE *fp; + char buf[HYBBX_CONFIG_LINE_MAX]; + enum { + SEC_NONE, + SEC_RIGHTS, + SEC_ALIASES, + SEC_COMMANDS + } section = SEC_NONE; + enum { + RIGHT_NONE, + RIGHT_USERCHANGE, + RIGHT_USERDELETE, + RIGHT_DELETE, + RIGHT_PROMOTE, + RIGHT_DEMOTE + } right_kind = RIGHT_NONE; + char cur_alias_canonical[HYBBX_CMD_VERB_MAX]; + hybbx_command_def_t *cmd = NULL; + target_right_rule_t *target_rule = NULL; + promote_right_rule_t *promote_rule = NULL; + demote_right_rule_t *demote_rule = NULL; + + if (reg == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + cur_alias_canonical[0] = '\0'; + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + while (fgets(buf, sizeof(buf), fp) != NULL) { + char *line = buf; + char *colon; + unsigned indent; + + indent = line_indent(line); + trim_inplace(line); + if (line[0] == '\0' || line[0] == '#') { + continue; + } + + if (indent == 0 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + cmd = NULL; + target_rule = NULL; + promote_rule = NULL; + demote_rule = NULL; + + if (str_ieq(line, "rights")) { + section = SEC_RIGHTS; + right_kind = RIGHT_NONE; + } else if (str_ieq(line, "aliases")) { + section = SEC_ALIASES; + right_kind = RIGHT_NONE; + } else if (str_ieq(line, "commands")) { + section = SEC_COMMANDS; + right_kind = RIGHT_NONE; + } else { + section = SEC_NONE; + } + continue; + } + + if (section == SEC_RIGHTS) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + target_rule = NULL; + promote_rule = NULL; + demote_rule = NULL; + + if (str_ieq(line_key(line), "userchange")) { + right_kind = RIGHT_USERCHANGE; + } else if (str_ieq(line_key(line), "userdelete")) { + right_kind = RIGHT_USERDELETE; + } else if (str_ieq(line_key(line), "delete")) { + right_kind = RIGHT_DELETE; + } else if (str_ieq(line_key(line), "promote")) { + right_kind = RIGHT_PROMOTE; + } else if (str_ieq(line_key(line), "demote")) { + right_kind = RIGHT_DEMOTE; + } else { + right_kind = RIGHT_NONE; + } + continue; + } + + if (indent == 4 && line[0] == '-') { + if (right_kind == RIGHT_USERCHANGE && + reg->userchange_rule_count < HYBBX_RIGHTS_TARGET_RULES_MAX) { + target_rule = + ®->userchange_rules[reg->userchange_rule_count++]; + memset(target_rule, 0, sizeof(*target_rule)); + } else if (right_kind == RIGHT_USERDELETE && + reg->userdelete_rule_count < + HYBBX_RIGHTS_TARGET_RULES_MAX) { + target_rule = + ®->userdelete_rules[reg->userdelete_rule_count++]; + memset(target_rule, 0, sizeof(*target_rule)); + } else if (right_kind == RIGHT_DELETE && + reg->delete_rule_count < + HYBBX_RIGHTS_TARGET_RULES_MAX) { + target_rule = + ®->delete_rules[reg->delete_rule_count++]; + memset(target_rule, 0, sizeof(*target_rule)); + } else if (right_kind == RIGHT_PROMOTE && + reg->promote_rule_count < + HYBBX_RIGHTS_PROMOTE_RULES_MAX) { + promote_rule = + ®->promote_rules[reg->promote_rule_count++]; + memset(promote_rule, 0, sizeof(*promote_rule)); + } else if (right_kind == RIGHT_DEMOTE && + reg->demote_rule_count < + HYBBX_RIGHTS_DEMOTE_RULES_MAX) { + demote_rule = + ®->demote_rules[reg->demote_rule_count++]; + memset(demote_rule, 0, sizeof(*demote_rule)); + } + continue; + } + + if (indent >= 6) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + + if (target_rule != NULL) { + if (str_ieq(line_key(line), "actor")) { + target_rule->actor = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "targets")) { + parse_level_list(colon + 1, target_rule->targets, 5, + &target_rule->target_count); + } + } else if (promote_rule != NULL) { + if (str_ieq(line_key(line), "actor")) { + promote_rule->actor = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "to")) { + promote_rule->to_level = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "from")) { + parse_level_list(colon + 1, promote_rule->from_levels, + 4, &promote_rule->from_count); + } + } else if (demote_rule != NULL) { + if (str_ieq(line_key(line), "actor")) { + demote_rule->actor = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "from")) { + parse_level_list(colon + 1, demote_rule->from_levels, + 4, &demote_rule->from_count); + } + } + } + continue; + } + + if (section == SEC_ALIASES) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + alias_entry_t *entry; + + line[strlen(line) - 1] = '\0'; + if (reg->alias_count >= HYBBX_COMMANDS_ALIASES_MAX) { + continue; + } + entry = ®->aliases[reg->alias_count++]; + hybbx_strlcpy(entry->canonical, line_key(line), + sizeof(entry->canonical)); + hybbx_strlcpy(cur_alias_canonical, line_key(line), + sizeof(cur_alias_canonical)); + continue; + } + if (indent >= 4 && line[0] == '-' && cur_alias_canonical[0] != '\0') { + alias_entry_t *entry = ®->aliases[reg->alias_count - 1]; + + if (entry->alias_count < HYBBX_COMMANDS_ALIAS_PER) { + line++; + trim_inplace(line); + strip_quotes(line); + hybbx_strlcpy(entry->aliases[entry->alias_count], line, + HYBBX_CMD_VERB_MAX); + entry->alias_count++; + } + } + continue; + } + + if (section == SEC_COMMANDS) { + if (indent == 2 && line[strlen(line) - 1] == ':') { + line[strlen(line) - 1] = '\0'; + if (reg->command_count < HYBBX_COMMANDS_MAX) { + cmd = ®->commands[reg->command_count++]; + memset(cmd, 0, sizeof(*cmd)); + cmd->min_level = HYBBX_LEVEL_GUEST; + hybbx_strlcpy(cmd->verb, line_key(line), sizeof(cmd->verb)); + } + continue; + } + if (indent >= 4 && cmd != NULL) { + colon = strchr(line, ':'); + if (colon == NULL) { + continue; + } + *colon = '\0'; + strip_quotes(colon + 1); + if (str_ieq(line_key(line), "group")) { + hybbx_strlcpy(cmd->group, colon + 1, sizeof(cmd->group)); + } else if (str_ieq(line_key(line), "min")) { + cmd->min_level = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "max")) { + cmd->max_level = level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "only")) { + cmd->only_level = (int)level_from_name(colon + 1); + } else if (str_ieq(line_key(line), "line1")) { + hybbx_strlcpy(cmd->line1, colon + 1, sizeof(cmd->line1)); + } else if (str_ieq(line_key(line), "line2")) { + hybbx_strlcpy(cmd->line2, colon + 1, sizeof(cmd->line2)); + } + } + } + } + + fclose(fp); + + if (reg->command_count == 0) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static void build_alias_lines(commands_registry_t *reg) +{ + char line[HYBBX_LINE_MAX]; + unsigned i; + unsigned j; + unsigned off; + + reg->alias_line_count = 0; + + for (i = 0; i < reg->alias_count && reg->alias_line_count < HYBBX_COMMANDS_ALIAS_LINES; + i++) { + const alias_entry_t *entry = ®->aliases[i]; + + off = 0; + line[0] = '\0'; + + for (j = 0; j < entry->alias_count; j++) { + const char *alias = entry->aliases[j]; + int n; + + if (strchr(alias, ' ') != NULL) { + n = snprintf(line + off, sizeof(line) - off, "%s%s", + off > 0 ? " " : "", alias); + } else { + n = snprintf(line + off, sizeof(line) - off, "%s%s -> %s", + off > 0 ? " " : "", alias, entry->canonical); + } + + if (n < 0 || (size_t)n >= sizeof(line) - off) { + if (reg->alias_line_count < HYBBX_COMMANDS_ALIAS_LINES) { + hybbx_strlcpy(reg->alias_lines[reg->alias_line_count++], + line, sizeof(reg->alias_lines[0])); + } + off = 0; + line[0] = '\0'; + j--; + continue; + } + off += (unsigned)n; + } + + if (off > 0 && reg->alias_line_count < HYBBX_COMMANDS_ALIAS_LINES) { + hybbx_strlcpy(reg->alias_lines[reg->alias_line_count++], + line, sizeof(reg->alias_lines[0])); + } + } +} + +static hybbx_result_t load_yaml_file(const char *rel_share, + hybbx_result_t (*parse_fn)(commands_registry_t *, + const char *), + const char *label) +{ + char path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (hybbx_path_resolve(path, sizeof(path), rel_share) == HYBBX_OK) { + rc = parse_fn(&g_registry, path); + if (rc == HYBBX_OK) { + hybbx_log_info("[commands] loaded %s (%s)", path, label); + return HYBBX_OK; + } + } + + return HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_commands_registry_init(void) +{ + hybbx_result_t rc; + + hybbx_commands_registry_shutdown(); + + rc = load_yaml_file(HYBBX_FILE_AREAS, parse_areas_yaml, "areas"); + if (rc != HYBBX_OK) { + rc = load_yaml_file("share/areas.yaml", parse_areas_yaml, "areas"); + } + if (rc != HYBBX_OK) { + hybbx_log_warn("[commands] failed to load " HYBBX_FILE_AREAS); + return HYBBX_ERR_IO; + } + + rc = load_yaml_file(HYBBX_FILE_COMMANDS, parse_commands_yaml, "commands"); + if (rc != HYBBX_OK) { + rc = load_yaml_file("share/commands.yaml", parse_commands_yaml, "commands"); + } + if (rc != HYBBX_OK) { + hybbx_log_warn("[commands] failed to load " HYBBX_FILE_COMMANDS); + hybbx_commands_registry_shutdown(); + return HYBBX_ERR_IO; + } + + build_alias_lines(&g_registry); + g_registry.loaded = 1; + hybbx_log_info("[commands] registry ready (%u areas, %u verbs)", + g_registry.area_count, g_registry.command_count); + return HYBBX_OK; +} + +void hybbx_commands_registry_shutdown(void) +{ + memset(&g_registry, 0, sizeof(g_registry)); +} + +const hybbx_command_def_t *hybbx_commands_registry_find(const char *verb) +{ + unsigned i; + + if (!g_registry.loaded || verb == NULL) { + return NULL; + } + + for (i = 0; i < g_registry.command_count; i++) { + if (str_ieq(g_registry.commands[i].verb, verb)) { + return &g_registry.commands[i]; + } + } + + return NULL; +} + +const char *hybbx_commands_registry_canonical(const char *topic) +{ + unsigned i; + unsigned j; + + if (!g_registry.loaded || topic == NULL || topic[0] == '\0') { + return topic; + } + + if (hybbx_commands_registry_find(topic) != NULL) { + return topic; + } + + for (i = 0; i < g_registry.alias_count; i++) { + for (j = 0; j < g_registry.aliases[i].alias_count; j++) { + const char *alias = g_registry.aliases[i].aliases[j]; + + if (strchr(alias, ' ') != NULL) { + continue; + } + if (str_ieq(alias, topic)) { + return g_registry.aliases[i].canonical; + } + } + } + + return topic; +} + +int hybbx_commands_registry_verb_allowed(hybbx_user_level_t level, + const char *verb) +{ + const char *canonical; + const hybbx_command_def_t *def; + + if (verb == NULL || verb[0] == '\0') { + return 1; + } + + if (!g_registry.loaded) { + return 0; + } + + canonical = hybbx_commands_registry_canonical(verb); + def = hybbx_commands_registry_find(canonical); + if (def == NULL) { + return 0; + } + + return command_level_allowed(def, level); +} + +int hybbx_commands_registry_help_allowed(hybbx_user_level_t level, + const char *verb) +{ + return hybbx_commands_registry_verb_allowed(level, verb); +} + +int hybbx_commands_registry_may_userchange(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + if (!g_registry.loaded) { + return 0; + } + + return target_right_allowed(g_registry.userchange_rules, + g_registry.userchange_rule_count, + actor, target); +} + +int hybbx_commands_registry_may_userdelete(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + unsigned i; + + if (!g_registry.loaded || hybbx_user_level_is_sysop(target)) { + return 0; + } + + for (i = 0; i < g_registry.userdelete_rule_count; i++) { + const target_right_rule_t *rule = &g_registry.userdelete_rules[i]; + + if (rule->actor == actor && + level_in_list(target, rule->targets, rule->target_count)) { + return 1; + } + } + + return 0; +} + +int hybbx_commands_registry_may_delete(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + if (!g_registry.loaded) { + return 0; + } + + if (target == HYBBX_LEVEL_SYSOP) { + return 0; + } + + return target_right_allowed(g_registry.delete_rules, + g_registry.delete_rule_count, + actor, target); +} + +int hybbx_commands_registry_may_promote(hybbx_user_level_t actor, + hybbx_user_level_t target, + int target_active, + hybbx_user_level_t new_level) +{ + unsigned i; + + if (!g_registry.loaded || !target_active || + hybbx_user_level_is_guest(target) || + target == HYBBX_LEVEL_SYSOP) { + return 0; + } + + for (i = 0; i < g_registry.promote_rule_count; i++) { + const promote_right_rule_t *rule = &g_registry.promote_rules[i]; + + if (rule->actor == actor && rule->to_level == new_level && + level_in_list(target, rule->from_levels, rule->from_count)) { + return 1; + } + } + + return 0; +} + +int hybbx_commands_registry_may_demote(hybbx_user_level_t actor, + hybbx_user_level_t target) +{ + unsigned i; + + if (!g_registry.loaded) { + return 0; + } + + for (i = 0; i < g_registry.demote_rule_count; i++) { + const demote_right_rule_t *rule = &g_registry.demote_rules[i]; + + if (rule->actor == actor && + level_in_list(target, rule->from_levels, rule->from_count)) { + return 1; + } + } + + return 0; +} + +#define MENU_LABEL_PREFIX_LEN 13u +#define MENU_CMD_FIELD_MAX (HYBBX_LINE_MAX - MENU_LABEL_PREFIX_LEN) + +static void emit_menu_line(hybbx_session_t *session, const char *label, + const char *cmds, int continuation) +{ + char buf[HYBBX_LINE_MAX]; + + if (continuation) { + snprintf(buf, sizeof(buf), " %s", cmds); + } else { + snprintf(buf, sizeof(buf), " %-10s %s", label, cmds); + } + hybbx_session_write_line(session, buf); +} + +static void format_verbs_line(char *out, size_t out_len, + const char verbs[][HYBBX_CMD_VERB_MAX], + unsigned count) +{ + size_t off = 0; + unsigned i; + + out[0] = '\0'; + for (i = 0; i < count; i++) { + int n; + + if (verbs[i][0] == '\0') { + continue; + } + n = snprintf(out + off, out_len - off, "%s/%s", + off > 0 ? " " : "", verbs[i]); + if (n < 0 || (size_t)n >= out_len - off) { + break; + } + off += (size_t)n; + } +} + +static void emit_verbs_wrapped(hybbx_session_t *session, const char *label, + const char verbs[][HYBBX_CMD_VERB_MAX], + unsigned count) +{ + char chunk[MENU_CMD_FIELD_MAX + 1]; + size_t off = 0; + unsigned physical_line = 0; + unsigned i; + + for (i = 0; i < count; i++) { + char token[HYBBX_CMD_VERB_MAX + 4]; + int n; + + if (verbs[i][0] == '\0') { + continue; + } + + n = snprintf(token, sizeof(token), "%s/%s", + off > 0 ? " " : "", verbs[i]); + if (n < 0) { + continue; + } + + if (off > 0 && (size_t)n + off > MENU_CMD_FIELD_MAX) { + chunk[off] = '\0'; + emit_menu_line(session, physical_line == 0 ? label : "", chunk, + physical_line > 0); + physical_line++; + off = 0; + n = snprintf(token, sizeof(token), "/%s", verbs[i]); + if (n < 0) { + continue; + } + } + + if ((size_t)n >= sizeof(chunk) - off) { + break; + } + + memcpy(chunk + off, token, (size_t)n); + off += (size_t)n; + } + + if (off > 0) { + chunk[off] = '\0'; + emit_menu_line(session, physical_line == 0 ? label : "", chunk, + physical_line > 0); + } +} + +static unsigned collect_area_verbs(const menu_area_t *area, + const hybbx_session_t *session, + hybbx_user_level_t level, + int filter_access, + char out[][HYBBX_CMD_VERB_MAX], + unsigned max) +{ + unsigned count = 0; + unsigned i; + unsigned j; + + if (area == NULL) { + return 0; + } + + for (i = 0; i < area->verb_count && count < max; i++) { + if (filter_access && + !command_verb_visible(session, level, area->verbs[i])) { + continue; + } + hybbx_strlcpy(out[count], area->verbs[i], HYBBX_CMD_VERB_MAX); + count++; + } + + for (i = 0; i < area->subarea_count; i++) { + const menu_subarea_t *sub = &area->subareas[i]; + + for (j = 0; j < sub->verb_count && count < max; j++) { + if (filter_access && + !command_verb_visible(session, level, sub->verbs[j])) { + continue; + } + hybbx_strlcpy(out[count], sub->verbs[j], HYBBX_CMD_VERB_MAX); + count++; + } + } + + return count; +} + +static void render_area(hybbx_session_t *session, const menu_area_t *area, + hybbx_user_level_t level, int filter_access) +{ + char cmds[HYBBX_LINE_MAX]; + char verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned count; + unsigned i; + + if (area == NULL) { + return; + } + + count = collect_area_verbs(area, session, level, filter_access, verbs, + HYBBX_COMMANDS_VERBS_PER_GROUP); + if (count == 0) { + return; + } + + emit_verbs_wrapped(session, area->label, verbs, count); + + if (!filter_access) { + for (i = 0; i < area->subarea_count; i++) { + const menu_subarea_t *sub = &area->subareas[i]; + char sub_verbs[HYBBX_COMMANDS_VERBS_PER_GROUP][HYBBX_CMD_VERB_MAX]; + unsigned sub_count = 0; + unsigned j; + + for (j = 0; j < sub->verb_count; j++) { + hybbx_strlcpy(sub_verbs[sub_count], sub->verbs[j], + HYBBX_CMD_VERB_MAX); + sub_count++; + } + + if (sub_count == 0) { + continue; + } + + format_verbs_line(cmds, sizeof(cmds), + (const char (*)[HYBBX_CMD_VERB_MAX])sub_verbs, + sub_count); + emit_menu_line(session, "", cmds, 1); + } + } +} + +static void render_area_labels(hybbx_session_t *session, + const char labels[][16], + unsigned label_count, + hybbx_user_level_t level, + int filter_access) +{ + unsigned i; + + for (i = 0; i < label_count; i++) { + const menu_area_t *area = area_find(labels[i]); + + if (area != NULL) { + render_area(session, area, level, filter_access); + } + } +} + +static unsigned menu_layout_index(hybbx_user_level_t level) +{ + const char *level_name = menu_level_name(level); + unsigned i; + + for (i = 0; i < g_registry.menu_level_count; i++) { + if (str_ieq(g_registry.menu_levels[i], level_name)) { + return i; + } + } + + return g_registry.menu_level_count; +} + +void hybbx_commands_registry_show_menu(hybbx_session_t *session) +{ + hybbx_user_level_t level; + unsigned layout; + + if (session == NULL || !g_registry.loaded) { + return; + } + + level = hybbx_session_user_level(session); + layout = menu_layout_index(level); + + hybbx_session_write_line(session, g_registry.menu_header); + if (layout < g_registry.menu_level_count) { + render_area_labels(session, + (const char (*)[16]) + g_registry.menu_area_labels[layout], + g_registry.menu_area_count[layout], + level, 1); + } + + /* + * Allow-list grants are not Sysop — their menu layout has no Sysop area. + * Append filtered Sysop area so /monitor is visible when granted. + */ + if (hybbx_monitor_session_may_use(session) && + !hybbx_user_level_is_sysop(level)) { + const menu_area_t *sysop = area_find("Sysop"); + + if (sysop != NULL) { + render_area(session, sysop, level, 1); + } + } +} + +void hybbx_commands_registry_show_index(hybbx_session_t *session) +{ + hybbx_user_level_t level; + + if (session == NULL || !g_registry.loaded) { + return; + } + + level = hybbx_session_user_level(session); + hybbx_session_write_line(session, g_registry.index_header); + /* Filter by access — Sysop cmds (incl. /monitor) only for Sysop + grants. */ + render_area_labels(session, + (const char (*)[16])g_registry.index_labels, + g_registry.index_label_count, + level, 1); +} + +void hybbx_commands_registry_show_aliases(hybbx_session_t *session) +{ + hybbx_user_level_t level; + unsigned i; + + if (session == NULL || !g_registry.loaded) { + return; + } + + level = hybbx_session_user_level(session); + hybbx_session_write_line(session, g_registry.alias_header); + for (i = 0; i < g_registry.alias_count; i++) { + const alias_entry_t *entry = &g_registry.aliases[i]; + char line[HYBBX_LINE_MAX]; + unsigned j; + unsigned off = 0; + + if (!command_verb_visible(session, level, entry->canonical)) { + continue; + } + + line[0] = '\0'; + for (j = 0; j < entry->alias_count; j++) { + const char *alias = entry->aliases[j]; + int n; + + if (strchr(alias, ' ') != NULL) { + n = snprintf(line + off, sizeof(line) - off, "%s%s", + off > 0 ? " " : "", alias); + } else { + n = snprintf(line + off, sizeof(line) - off, "%s%s -> %s", + off > 0 ? " " : "", alias, entry->canonical); + } + if (n < 0 || (size_t)n >= sizeof(line) - off) { + break; + } + off += (unsigned)n; + } + if (off > 0) { + hybbx_session_write_line(session, line); + } + } +} + +void hybbx_commands_registry_show_help(hybbx_session_t *session, + const char *canonical) +{ + const hybbx_command_def_t *def; + + if (session == NULL || canonical == NULL || !g_registry.loaded) { + return; + } + + def = hybbx_commands_registry_find(canonical); + if (def == NULL || def->line1[0] == '\0') { + return; + } + + hybbx_session_write_line(session, def->line1); + if (def->line2[0] != '\0') { + hybbx_session_write_line(session, def->line2); + } +} + +void hybbx_commands_registry_show_sysop_cmds(hybbx_session_t *session) +{ + unsigned i; + + if (session == NULL || !g_registry.loaded) { + return; + } + + for (i = 0; i < g_registry.command_count; i++) { + const hybbx_command_def_t *def = &g_registry.commands[i]; + + /* Sysop=1, Admin=2 — only the admin/sysop privilege set. */ + if (def->min_level == 0 || def->min_level > HYBBX_LEVEL_ADMIN) { + continue; + } + if (def->line1[0] == '\0') { + continue; + } + hybbx_session_write_line(session, def->line1); + } +} diff --git a/src/core/conference.c b/src/core/conference.c new file mode 100644 index 0000000..890348f --- /dev/null +++ b/src/core/conference.c @@ -0,0 +1,720 @@ +#include "hybbx/conference.h" +#include "hybbx/chat.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/monitor.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" + +#include +#include +#include + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int invite_reply_yes(const char *line) +{ + return line != NULL && + (str_ieq(line, "y") || str_ieq(line, "yes")); +} + +static int invite_reply_no(const char *line) +{ + return line != NULL && + (str_ieq(line, "n") || str_ieq(line, "no")); +} + +static int topic_char_ok(char ch) +{ + return (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == ' ' || ch == '-' || ch == '_'; +} + +static int conference_topic_valid(const char *topic) +{ + size_t len; + size_t i; + + if (topic == NULL) { + return 0; + } + + len = strlen(topic); + if (len < 2 || len >= HYBBX_CONFERENCE_TOPIC_MAX) { + return 0; + } + + for (i = 0; i < len; i++) { + if (!topic_char_ok(topic[i])) { + return 0; + } + } + + return 1; +} + +typedef struct find_user_session_ctx { + const char *username; + hybbx_session_t *found; +} find_user_session_ctx_t; + +static void find_user_session_visitor(hybbx_session_t *session, void *userdata) +{ + find_user_session_ctx_t *ctx = (find_user_session_ctx_t *)userdata; + + if (ctx == NULL || session == NULL || ctx->found != NULL) { + return; + } + + if (!hybbx_session_logged_in(session) || hybbx_session_is_guest(session)) { + return; + } + + if (str_ieq(hybbx_session_username(session), ctx->username)) { + ctx->found = session; + } +} + +static hybbx_session_t *conference_find_online_user(hybbx_service_t *service, + const char *username) +{ + find_user_session_ctx_t ctx; + + if (service == NULL || username == NULL || username[0] == '\0') { + return NULL; + } + + ctx.username = username; + ctx.found = NULL; + hybbx_service_visit_sessions(service, find_user_session_visitor, &ctx); + return ctx.found; +} + +static int conference_session_active(const hybbx_session_t *session) +{ + return session != NULL && + hybbx_session_area(session) == HYBBX_AREA_CONFERENCE; +} + +typedef struct invite_cancel_ctx { + const char *from_username; +} invite_cancel_ctx_t; + +static void invite_cancel_visitor(hybbx_session_t *session, void *userdata) +{ + invite_cancel_ctx_t *ctx = (invite_cancel_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || ctx->from_username == NULL) { + return; + } + + if (!hybbx_conference_invite_pending(session)) { + return; + } + + if (!str_ieq(hybbx_session_conference_invite_from(session), + ctx->from_username)) { + return; + } + + hybbx_session_clear_conference_invite(session); + hybbx_session_write_line(session, "*** Conference invite cancelled."); +} + +static void conference_cancel_invites_from(hybbx_service_t *service, + const char *from_username) +{ + invite_cancel_ctx_t ctx; + + if (service == NULL || from_username == NULL || from_username[0] == '\0') { + return; + } + + ctx.from_username = from_username; + hybbx_service_visit_sessions(service, invite_cancel_visitor, &ctx); +} + +static void conference_notify_partner_left(hybbx_session_t *session, + const char *partner_username) +{ + hybbx_service_t *service; + hybbx_session_t *partner; + char line[HYBBX_LINE_MAX]; + + if (session == NULL || partner_username == NULL || + partner_username[0] == '\0') { + return; + } + + service = hybbx_session_service(session); + if (service == NULL) { + return; + } + + partner = conference_find_online_user(service, partner_username); + if (partner == NULL || !conference_session_active(partner)) { + return; + } + + if (!str_ieq(hybbx_session_conference_partner(partner), + hybbx_session_username(session))) { + return; + } + + hybbx_session_clear_conference(partner); + snprintf(line, sizeof(line), "*** %s left the conference.", + hybbx_session_display_name(session)); + hybbx_session_write_line(partner, line); + if (hybbx_session_area(partner) == HYBBX_AREA_CONFERENCE) { + (void)hybbx_session_leave_area(partner); + } +} + +static hybbx_result_t conference_activate(hybbx_service_t *service, + hybbx_session_t *initiator, + hybbx_session_t *partner, + const char *topic) +{ + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (service == NULL || initiator == NULL || partner == NULL || + topic == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_session_join_conference(initiator, topic, + hybbx_session_username(partner)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = hybbx_session_join_conference(partner, topic, + hybbx_session_username(initiator)); + if (rc != HYBBX_OK) { + hybbx_session_clear_conference(initiator); + (void)hybbx_session_leave_area(initiator); + return rc; + } + + snprintf(line, sizeof(line), "Conference: %s", topic); + hybbx_session_write_line(initiator, line); + hybbx_session_write_line(initiator, + "Each line is a message; /leave or /main to exit."); + + snprintf(line, sizeof(line), "Conference: %s", topic); + hybbx_session_write_line(partner, line); + hybbx_session_write_line(partner, + "Each line is a message; /leave or /main to exit."); + + return HYBBX_OK; +} + +hybbx_result_t hybbx_conference_start(hybbx_service_t *service, + hybbx_session_t *initiator, + const char *topic, + const char *partner_spec) +{ + hybbx_storage_t *storage; + hybbx_user_record_t partner_user; + hybbx_session_t *partner_session; + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (service == NULL || initiator == NULL || topic == NULL || + partner_spec == NULL || topic[0] == '\0' || partner_spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (hybbx_session_is_guest(initiator)) { + hybbx_session_write_line(initiator, "Guests cannot use conference."); + return HYBBX_ERR_DENIED; + } + + if (!conference_topic_valid(topic)) { + hybbx_session_write_line(initiator, "Invalid conference topic."); + return HYBBX_ERR_INVALID; + } + + if (conference_session_active(initiator)) { + hybbx_session_write_line(initiator, "Already in a conference."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(initiator)) { + hybbx_session_write_line(initiator, + "Answer the pending conference invite first (y/n)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, partner_spec, &partner_user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(initiator, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (!partner_user.active) { + hybbx_session_write_line(initiator, "User is not active."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_guest(partner_user.level)) { + hybbx_session_write_line(initiator, "Cannot conference with a guest."); + return HYBBX_OK; + } + + if (str_ieq(partner_user.username, hybbx_session_username(initiator))) { + hybbx_session_write_line(initiator, "Cannot conference with yourself."); + return HYBBX_OK; + } + + if (!hybbx_session_conference_may_invite(initiator, partner_user.username)) { + hybbx_session_write_line(initiator, + "Invite limit: 2 per user per 30 minutes."); + return HYBBX_OK; + } + + partner_session = conference_find_online_user(service, partner_user.username); + if (partner_session == NULL) { + hybbx_session_write_line(initiator, "User is not online."); + return HYBBX_OK; + } + + if (conference_session_active(partner_session)) { + hybbx_session_write_line(initiator, "User is already in a conference."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(partner_session)) { + hybbx_session_write_line(initiator, + "User has a pending conference invite."); + return HYBBX_OK; + } + + hybbx_session_set_conference_invite(partner_session, + hybbx_session_username(initiator), + topic); + hybbx_session_conference_invite_sent(initiator, partner_user.username); + + snprintf(line, sizeof(line), "Conference invite: %s", topic); + (void)hybbx_msg_send_private(partner_session, + hybbx_session_display_name(initiator), line); + hybbx_session_write_line(partner_session, "Accept? y/n or yes/no"); + + snprintf(line, sizeof(line), "Conference invite sent to %s.", + hybbx_user_display_name(&partner_user)); + hybbx_session_write_line(initiator, line); + + return HYBBX_OK; +} + +hybbx_result_t hybbx_conference_reply_invite(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char from_username[HYBBX_USER_NAME_MAX]; + char topic[HYBBX_CONFERENCE_TOPIC_MAX]; + hybbx_session_t *initiator; + char msg[HYBBX_LINE_MAX]; + + if (service == NULL || session == NULL || line == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_conference_invite_pending(session)) { + return HYBBX_ERR_INVALID; + } + + if (!invite_reply_yes(line) && !invite_reply_no(line)) { + return HYBBX_ERR_NOT_FOUND; + } + + { + const char *from_ptr = hybbx_session_conference_invite_from(session); + const char *topic_ptr = hybbx_session_conference_invite_topic(session); + + if (from_ptr == NULL || from_ptr[0] == '\0' || + topic_ptr == NULL || topic_ptr[0] == '\0') { + hybbx_session_clear_conference_invite(session); + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(from_username, from_ptr, sizeof(from_username)); + hybbx_strlcpy(topic, topic_ptr, sizeof(topic)); + } + + hybbx_session_clear_conference_invite(session); + + if (invite_reply_no(line)) { + initiator = conference_find_online_user(service, from_username); + if (initiator != NULL) { + snprintf(msg, sizeof(msg), + "meet status=declined user=%s", + hybbx_session_display_name(session)); + (void)hybbx_msg_send_system(initiator, msg); + } + hybbx_session_write_line(session, "*** Conference invite declined."); + return HYBBX_OK; + } + + if (conference_session_active(session)) { + hybbx_session_write_line(session, "Already in a conference."); + return HYBBX_OK; + } + + initiator = conference_find_online_user(service, from_username); + if (initiator == NULL) { + hybbx_session_write_line(session, "Inviter is no longer online."); + return HYBBX_OK; + } + + if (conference_session_active(initiator)) { + hybbx_session_write_line(session, "Inviter is already in a conference."); + return HYBBX_OK; + } + + if (conference_activate(service, initiator, session, topic) != HYBBX_OK) { + hybbx_session_write_line(session, "Conference could not be started."); + return HYBBX_OK; + } + + snprintf(msg, sizeof(msg), "meet status=accepted user=%s", + hybbx_session_display_name(session)); + hybbx_session_write_line(initiator, msg); + if (hybbx_monitor_is_active(initiator)) { + hybbx_monitor_broadcast(service, msg); + } + + return HYBBX_OK; +} + +void hybbx_conference_invite_tick(hybbx_service_t *service, + hybbx_session_t *session) +{ + time_t deadline; + time_t now; + char from_username[HYBBX_USER_NAME_MAX]; + const char *from_ptr; + hybbx_session_t *initiator; + char msg[HYBBX_LINE_MAX]; + + if (service == NULL || session == NULL) { + return; + } + + if (!hybbx_conference_invite_pending(session)) { + return; + } + + deadline = hybbx_session_conference_invite_deadline(session); + if (deadline == 0) { + return; + } + + now = time(NULL); + if (now < deadline) { + return; + } + + from_ptr = hybbx_session_conference_invite_from(session); + if (from_ptr != NULL) { + hybbx_strlcpy(from_username, from_ptr, sizeof(from_username)); + } else { + from_username[0] = '\0'; + } + + hybbx_session_clear_conference_invite(session); + hybbx_session_write_line(session, "*** Conference invite timed out."); + + if (from_username[0] != '\0') { + initiator = conference_find_online_user(service, from_username); + if (initiator != NULL) { + snprintf(msg, sizeof(msg), "meet status=timeout user=%s", + hybbx_session_display_name(session)); + hybbx_session_write_line(initiator, msg); + if (hybbx_monitor_is_active(initiator)) { + hybbx_monitor_broadcast(service, msg); + } + } + } +} + +hybbx_result_t hybbx_conference_monitor_meet(hybbx_service_t *service, + hybbx_session_t *initiator, + const char *partner_spec, + int force) +{ + const hybbx_monitor_config_t *mcfg; + hybbx_storage_t *storage; + hybbx_user_record_t partner_user; + hybbx_session_t *partner_session; + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + const char *topic = "monitor"; + unsigned timeout_sec = 20u; + + if (service == NULL || initiator == NULL || partner_spec == NULL || + partner_spec[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + mcfg = hybbx_monitor_config_get(); + if (mcfg != NULL) { + timeout_sec = mcfg->invite_timeout_sec; + } + + if (hybbx_session_is_guest(initiator)) { + hybbx_session_write_line(initiator, "Guests cannot use meet."); + return HYBBX_ERR_DENIED; + } + + if (conference_session_active(initiator)) { + hybbx_session_write_line(initiator, "Already in a conference."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(initiator)) { + hybbx_session_write_line(initiator, + "Answer the pending conference invite first (y/n)."); + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, partner_spec, &partner_user); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(initiator, "Unknown user."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (!partner_user.active) { + hybbx_session_write_line(initiator, "User is not active."); + return HYBBX_OK; + } + + if (hybbx_user_level_is_guest(partner_user.level)) { + hybbx_session_write_line(initiator, "Cannot conference with a guest."); + return HYBBX_OK; + } + + if (str_ieq(partner_user.username, hybbx_session_username(initiator))) { + hybbx_session_write_line(initiator, "Cannot conference with yourself."); + return HYBBX_OK; + } + + partner_session = conference_find_online_user(service, partner_user.username); + if (partner_session == NULL) { + hybbx_session_write_line(initiator, "User is not online."); + return HYBBX_OK; + } + + if (conference_session_active(partner_session)) { + hybbx_session_write_line(initiator, "User is already in a conference."); + return HYBBX_OK; + } + + if (force) { + if (conference_activate(service, initiator, partner_session, + topic) != HYBBX_OK) { + hybbx_session_write_line(initiator, + "Conference could not be started."); + return HYBBX_OK; + } + snprintf(line, sizeof(line), "meet status=forced user=%s", + hybbx_user_display_name(&partner_user)); + hybbx_session_write_line(initiator, line); + hybbx_monitor_broadcast(service, line); + hybbx_session_write_line(partner_session, + "You were placed into a conference by Sysop."); + return HYBBX_OK; + } + + if (hybbx_conference_invite_pending(partner_session)) { + hybbx_session_write_line(initiator, + "User has a pending conference invite."); + return HYBBX_OK; + } + + hybbx_session_set_conference_invite(partner_session, + hybbx_session_username(initiator), + topic); + hybbx_session_set_conference_invite_deadline( + partner_session, time(NULL) + (time_t)timeout_sec); + + snprintf(line, sizeof(line), + "*** Conference invite from %s: %s (reply in %u s)", + hybbx_session_display_name(initiator), topic, timeout_sec); + hybbx_session_write_line(partner_session, line); + hybbx_session_write_line(partner_session, "*** Accept? y/n or yes/no"); + + snprintf(line, sizeof(line), + "meet status=pending user=%s timeout=%us", + hybbx_user_display_name(&partner_user), timeout_sec); + hybbx_session_write_line(initiator, line); + hybbx_monitor_broadcast(service, line); + + return HYBBX_OK; +} + +typedef struct conference_post_ctx { + hybbx_session_t *from; + const char *message; + const char *from_user; + const char *from_login; +} conference_post_ctx_t; + +static void conference_post_visitor(hybbx_session_t *session, void *userdata) +{ + conference_post_ctx_t *ctx = (conference_post_ctx_t *)userdata; + char line[HYBBX_USER_NAME_MAX + HYBBX_LINE_MAX + 16]; + const char *session_login; + + if (session == NULL || ctx == NULL || ctx->message == NULL || + ctx->from == NULL || ctx->from_login == NULL) { + return; + } + + if (!conference_session_active(session)) { + return; + } + + if (session == ctx->from) { + snprintf(line, sizeof(line), "ME: %s", ctx->message); + hybbx_session_write_line(session, line); + return; + } + + session_login = hybbx_session_username(session); + if (session_login == NULL || + !str_ieq(hybbx_session_conference_partner(session), ctx->from_login) || + !str_ieq(hybbx_session_conference_partner(ctx->from), session_login)) { + return; + } + + snprintf(line, sizeof(line), "%s: %s", ctx->from_user, ctx->message); + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_conference_post(hybbx_service_t *service, + hybbx_session_t *from, + const char *message) +{ + conference_post_ctx_t ctx; + const hybbx_chat_config_t *chat; + + if (service == NULL || from == NULL || message == NULL) { + return HYBBX_ERR_INVALID; + } + + if (message[0] == '\0') { + return HYBBX_OK; + } + + if (!conference_session_active(from)) { + return HYBBX_ERR_INVALID; + } + + chat = hybbx_service_get_chat(service); + if (chat == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(message) > chat->message_max) { + return HYBBX_ERR_INVALID; + } + + ctx.from = from; + ctx.message = message; + ctx.from_user = hybbx_session_display_name(from); + ctx.from_login = hybbx_session_username(from); + if (ctx.from_login == NULL || ctx.from_login[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_service_visit_sessions(service, conference_post_visitor, &ctx); + return HYBBX_OK; +} + +void hybbx_conference_area_leaving(hybbx_session_t *session) +{ + const char *partner_name; + + if (session == NULL) { + return; + } + + partner_name = hybbx_session_conference_partner(session); + hybbx_session_clear_conference(session); + + if (partner_name != NULL && partner_name[0] != '\0') { + conference_notify_partner_left(session, partner_name); + } +} + +void hybbx_conference_session_closed(hybbx_session_t *session) +{ + hybbx_service_t *service; + const char *username; + + if (session == NULL) { + return; + } + + if (hybbx_conference_invite_pending(session)) { + hybbx_session_clear_conference_invite(session); + } + + if (!conference_session_active(session)) { + service = hybbx_session_service(session); + username = hybbx_session_username(session); + if (service != NULL && username != NULL && username[0] != '\0') { + conference_cancel_invites_from(service, username); + } + return; + } + + hybbx_conference_area_leaving(session); + + service = hybbx_session_service(session); + username = hybbx_session_username(session); + if (service != NULL && username != NULL && username[0] != '\0') { + conference_cancel_invites_from(service, username); + } +} diff --git a/src/core/config.c b/src/core/config.c new file mode 100644 index 0000000..9b84d97 --- /dev/null +++ b/src/core/config.c @@ -0,0 +1,862 @@ +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include +#include +#include +#include + +#define HYBBX_CONFIG_INITIAL_CAP 32 + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static char *trim_inplace(char *s) +{ + char *end; + + if (s == NULL) { + return NULL; + } + + while (*s != '\0' && isspace((unsigned char)*s)) { + s++; + } + + if (*s == '\0') { + return s; + } + + end = s + strlen(s) - 1; + while (end > s && isspace((unsigned char)*end)) { + *end = '\0'; + end--; + } + + return s; +} + +static void strip_inline_comment_inplace(char *value) +{ + size_t i; + int in_single = 0; + int in_double = 0; + + if (value == NULL) { + return; + } + + for (i = 0; value[i] != '\0'; i++) { + char c = value[i]; + + if (!in_single && !in_double && (c == ';' || c == '#')) { + value[i] = '\0'; + break; + } + + if (!in_double && c == '\'') { + in_single = !in_single; + } else if (!in_single && c == '"') { + in_double = !in_double; + } + } + + { + char *end = value + strlen(value); + + while (end > value && isspace((unsigned char)*(end - 1))) { + *--end = '\0'; + } + } +} + +static int is_comment_line(const char *line) +{ + const char *p = line; + + while (*p != '\0' && isspace((unsigned char)*p)) { + p++; + } + + return *p == ';' || *p == '#'; +} + +static hybbx_result_t append_entry(hybbx_config_t *config, + const char *section, + const char *key, + const char *value) +{ + hybbx_config_entry_t *entry; + hybbx_config_entry_t *grown; + + if (section == NULL || key == NULL || value == NULL) { + return HYBBX_ERR_INVALID; + } + + if (strlen(section) >= HYBBX_CONFIG_SECTION_MAX || + strlen(key) >= HYBBX_CONFIG_KEY_MAX || + strlen(value) >= HYBBX_CONFIG_VALUE_MAX) { + return HYBBX_ERR_INVALID; + } + + if (config->count == 0) { + config->entries = malloc(HYBBX_CONFIG_INITIAL_CAP * sizeof(*config->entries)); + if (config->entries == NULL) { + return HYBBX_ERR_NOMEM; + } + } else if ((config->count % HYBBX_CONFIG_INITIAL_CAP) == 0) { + grown = realloc(config->entries, + (config->count + HYBBX_CONFIG_INITIAL_CAP) * + sizeof(*config->entries)); + if (grown == NULL) { + return HYBBX_ERR_NOMEM; + } + config->entries = grown; + } + + entry = &config->entries[config->count]; + entry->section = hybbx_strdup(section); + entry->key = hybbx_strdup(key); + entry->value = hybbx_strdup(value); + + if (entry->section == NULL || entry->key == NULL || entry->value == NULL) { + free(entry->section); + free(entry->key); + free(entry->value); + return HYBBX_ERR_NOMEM; + } + + config->count++; + return HYBBX_OK; +} + +hybbx_result_t hybbx_config_load(hybbx_config_t *config, const char *path) +{ + FILE *fp; + char line[HYBBX_CONFIG_LINE_MAX]; + char current_section[HYBBX_CONFIG_SECTION_MAX] = ""; + char *eq; + char *key; + char *value; + + if (config == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(config, 0, sizeof(*config)); + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *content; + size_t len; + + len = strlen(line); + if (len > 0 && line[len - 1] != '\n' && + len >= sizeof(line) - 1) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + content = trim_inplace(line); + + if (content[0] == '\0' || is_comment_line(content)) { + continue; + } + + if (content[0] == '[') { + char *closing = strchr(content, ']'); + + if (closing == NULL) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + *closing = '\0'; + strncpy(current_section, content + 1, sizeof(current_section) - 1); + current_section[sizeof(current_section) - 1] = '\0'; + trim_inplace(current_section); + continue; + } + + if (current_section[0] == '\0') { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + eq = strchr(content, '='); + if (eq == NULL) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_INVALID; + } + + *eq = '\0'; + key = trim_inplace(content); + value = trim_inplace(eq + 1); + strip_inline_comment_inplace(value); + + len = strlen(value); + if (len >= 2 && + ((value[0] == '"' && value[len - 1] == '"') || + (value[0] == '\'' && value[len - 1] == '\''))) { + value[len - 1] = '\0'; + value++; + } + + if (append_entry(config, current_section, key, value) != HYBBX_OK) { + fclose(fp); + hybbx_config_free(config); + return HYBBX_ERR_NOMEM; + } + } + + fclose(fp); + return HYBBX_OK; +} + +void hybbx_config_free(hybbx_config_t *config) +{ + size_t i; + + if (config == NULL) { + return; + } + + for (i = 0; i < config->count; i++) { + free(config->entries[i].section); + free(config->entries[i].key); + free(config->entries[i].value); + } + + free(config->entries); + config->entries = NULL; + config->count = 0; +} + +const char *hybbx_config_get(const hybbx_config_t *config, + const char *section, + const char *key, + const char *default_value) +{ + size_t i; + + if (config == NULL || section == NULL || key == NULL) { + return default_value; + } + + for (i = 0; i < config->count; i++) { + if (strcmp(config->entries[i].section, section) == 0 && + strcmp(config->entries[i].key, key) == 0) { + return config->entries[i].value; + } + } + + return default_value; +} + +int hybbx_config_get_bool(const hybbx_config_t *config, + const char *section, + const char *key, + int default_value) +{ + const char *value = hybbx_config_get(config, section, key, NULL); + + if (value == NULL) { + return default_value; + } + + return hybbx_parse_bool(value, default_value); +} + +unsigned hybbx_config_get_uint(const hybbx_config_t *config, + const char *section, + const char *key, + unsigned default_value, + unsigned min_value, + unsigned max_value) +{ + const char *value; + char *end; + unsigned long parsed; + + value = hybbx_config_get(config, section, key, NULL); + if (value == NULL || value[0] == '\0') { + return default_value; + } + + parsed = strtoul(value, &end, 10); + if (end == value || *end != '\0') { + return default_value; + } + + if (parsed < min_value) { + return min_value; + } + if (parsed > max_value) { + return max_value; + } + + return (unsigned)parsed; +} + +static int config_section_has_keys(const hybbx_config_t *config, + const char *section) +{ + size_t i; + + if (config == NULL || section == NULL) { + return 0; + } + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + + if (strcmp(entry->section, section) != 0) { + continue; + } + if (strcmp(entry->key, "enabled") == 0) { + continue; + } + return 1; + } + + return 0; +} + +static int config_suffix_is_digits(const char *suffix) +{ + if (suffix == NULL || suffix[0] == '\0') { + return 0; + } + + while (*suffix != '\0') { + if (!isdigit((unsigned char)*suffix)) { + return 0; + } + suffix++; + } + + return 1; +} + +int hybbx_config_resolve_transport_section(const hybbx_config_t *config, + const char *plugin_name, + char *out_section, + size_t out_size) +{ + char prefix[128]; + size_t prefix_len; + size_t i; + + if (config == NULL || plugin_name == NULL || out_section == NULL || + out_size == 0) { + return 0; + } + + snprintf(prefix, sizeof(prefix), "transport.%s", plugin_name); + if (config_section_has_keys(config, prefix)) { + hybbx_strlcpy(out_section, prefix, out_size); + return 1; + } + + prefix_len = strlen(prefix); + for (i = 0; i < config->count; i++) { + const char *sec = config->entries[i].section; + const char *suffix; + + if (sec == NULL || strncmp(sec, prefix, prefix_len) != 0) { + continue; + } + + suffix = sec + prefix_len; + if (!config_suffix_is_digits(suffix)) { + continue; + } + if (!config_section_has_keys(config, sec)) { + continue; + } + + hybbx_strlcpy(out_section, sec, out_size); + return 1; + } + + hybbx_strlcpy(out_section, prefix, out_size); + return 0; +} + +char *hybbx_config_format_section(const hybbx_config_t *config, + const char *section) +{ + size_t i; + size_t cap = 64; + size_t len = 0; + char *out; + int first = 1; + + if (config == NULL || section == NULL) { + return NULL; + } + + out = malloc(cap); + if (out == NULL) { + return NULL; + } + out[0] = '\0'; + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + size_t need; + + if (strcmp(entry->section, section) != 0) { + continue; + } + + if (strcmp(entry->key, "enabled") == 0) { + continue; + } + + need = strlen(entry->key) + strlen(entry->value) + 2; + if (!first) { + need++; + } + + if (len + need + 1 > cap) { + char *grown; + + while (len + need + 1 > cap) { + cap *= 2; + } + grown = realloc(out, cap); + if (grown == NULL) { + free(out); + return NULL; + } + out = grown; + } + + if (!first) { + out[len++] = ';'; + out[len] = '\0'; + } + + len += (size_t)snprintf(out + len, cap - len, "%s=%s", + entry->key, entry->value); + first = 0; + } + + return out; +} + +typedef struct format_transport_ctx { + const hybbx_config_t *config; + const char *prefix; + size_t prefix_len; + char **sections; + size_t count; + size_t cap; +} format_transport_ctx_t; + +static int format_transport_section_seen(format_transport_ctx_t *ctx, + const char *section) +{ + size_t i; + + for (i = 0; i < ctx->count; i++) { + if (strcmp(ctx->sections[i], section) == 0) { + return 1; + } + } + return 0; +} + +static hybbx_result_t format_transport_section_add(format_transport_ctx_t *ctx, + const char *section) +{ + char *copy; + + if (format_transport_section_seen(ctx, section)) { + return HYBBX_OK; + } + + if (ctx->count >= ctx->cap) { + size_t new_cap = ctx->cap == 0 ? 4 : ctx->cap * 2; + char **grown = realloc(ctx->sections, new_cap * sizeof(*grown)); + + if (grown == NULL) { + return HYBBX_ERR_NOMEM; + } + ctx->sections = grown; + ctx->cap = new_cap; + } + + copy = hybbx_strdup(section); + if (copy == NULL) { + return HYBBX_ERR_NOMEM; + } + + ctx->sections[ctx->count++] = copy; + return HYBBX_OK; +} + +char *hybbx_config_format_transport_sections(const hybbx_config_t *config, + const char *plugin_name) +{ + format_transport_ctx_t ctx; + char prefix[128]; + size_t i; + size_t len = 0; + size_t cap = 64; + char *out; + hybbx_result_t rc; + + if (config == NULL || plugin_name == NULL) { + return NULL; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.config = config; + snprintf(prefix, sizeof(prefix), "transport.%s", plugin_name); + ctx.prefix = prefix; + ctx.prefix_len = strlen(prefix); + + if (config_section_has_keys(config, prefix)) { + rc = format_transport_section_add(&ctx, prefix); + if (rc != HYBBX_OK) { + goto fail; + } + } + + for (i = 0; i < config->count; i++) { + const char *sec = config->entries[i].section; + const char *suffix; + + if (sec == NULL || strncmp(sec, prefix, ctx.prefix_len) != 0) { + continue; + } + + suffix = sec + ctx.prefix_len; + if (!config_suffix_is_digits(suffix)) { + continue; + } + if (!config_section_has_keys(config, sec)) { + continue; + } + + rc = format_transport_section_add(&ctx, sec); + if (rc != HYBBX_OK) { + goto fail; + } + } + + if (ctx.count == 0) { + for (i = 0; i < ctx.count; i++) { + free(ctx.sections[i]); + } + free(ctx.sections); + return hybbx_config_format_section(config, prefix); + } + + out = malloc(cap); + if (out == NULL) { + rc = HYBBX_ERR_NOMEM; + goto fail; + } + out[0] = '\0'; + + for (i = 0; i < ctx.count; i++) { + char *section_cfg = hybbx_config_format_section(config, ctx.sections[i]); + size_t part_len; + + if (section_cfg == NULL) { + free(out); + rc = HYBBX_ERR_NOMEM; + goto fail; + } + + part_len = strlen(section_cfg); + if (len > 0) { + if (len + 1 + part_len + 1 > cap) { + while (len + 1 + part_len + 1 > cap) { + cap *= 2; + } + { + char *grown = realloc(out, cap); + + if (grown == NULL) { + free(section_cfg); + free(out); + rc = HYBBX_ERR_NOMEM; + goto fail; + } + out = grown; + } + } + out[len++] = HYBBX_PACKET_RADIO_INSTANCE_SEP; + out[len] = '\0'; + } + + if (len + part_len + 1 > cap) { + while (len + part_len + 1 > cap) { + cap *= 2; + } + { + char *grown = realloc(out, cap); + + if (grown == NULL) { + free(section_cfg); + free(out); + rc = HYBBX_ERR_NOMEM; + goto fail; + } + out = grown; + } + } + + memcpy(out + len, section_cfg, part_len + 1); + len += part_len; + free(section_cfg); + } + + for (i = 0; i < ctx.count; i++) { + free(ctx.sections[i]); + } + free(ctx.sections); + return out; + +fail: + for (i = 0; i < ctx.count; i++) { + free(ctx.sections[i]); + } + free(ctx.sections); + return NULL; +} + +char *hybbx_config_prepend_packet_radio_max25(const hybbx_config_t *config, + const char *body) +{ + char prefix[HYBBX_CONFIG_VALUE_MAX * 2]; + char *out; + size_t prefix_len; + size_t body_len; + int check; + + if (config == NULL) { + return body != NULL ? hybbx_strdup(body) : NULL; + } + + if (body == NULL || body[0] == '\0') { + return hybbx_strdup(body != NULL ? body : ""); + } + + check = hybbx_config_get_bool(config, "max25", "check", 1); + snprintf(prefix, sizeof(prefix), + "max25_check=%s;max25_host=%s;max25_port=%u;max25_timeout_ms=%u", + check ? "yes" : "no", + hybbx_config_get(config, "max25", "host", "127.0.0.1"), + hybbx_config_get_uint(config, "max25", "port", + HYBBX_MAX25_DEFAULT_PORT, 1u, 65535u), + hybbx_config_get_uint(config, "max25", "timeout_ms", + HYBBX_MAX25_PROBE_TIMEOUT_MS, + 100u, 60000u)); + + if (body == NULL || body[0] == '\0') { + return hybbx_strdup(prefix); + } + + prefix_len = strlen(prefix); + body_len = strlen(body); + out = malloc(prefix_len + 1u + body_len + 1u); + if (out == NULL) { + return NULL; + } + + memcpy(out, prefix, prefix_len); + out[prefix_len] = HYBBX_PACKET_RADIO_INSTANCE_SEP; + memcpy(out + prefix_len + 1u, body, body_len + 1u); + return out; +} + +char *hybbx_config_format_packet_radio_start(const hybbx_config_t *config) +{ + char *body; + + body = hybbx_config_format_transport_sections(config, "packet_radio"); + if (body == NULL) { + return NULL; + } + + { + char *out = hybbx_config_prepend_packet_radio_max25(config, body); + + free(body); + return out; + } +} + +char *hybbx_config_format_baycom_start(const hybbx_config_t *config) +{ + char *body; + + body = hybbx_config_format_transport_sections(config, "baycom"); + if (body == NULL) { + return NULL; + } + + { + char *out = hybbx_config_prepend_packet_radio_max25(config, body); + + free(body); + return out; + } +} + +void hybbx_config_foreach(const hybbx_config_t *config, + hybbx_config_iter_fn fn, void *ctx) +{ + size_t i; + + if (config == NULL || fn == NULL) { + return; + } + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + + fn(entry->section, entry->key, entry->value, ctx); + } +} + +void hybbx_config_foreach_section(const hybbx_config_t *config, + hybbx_config_section_iter_fn fn, void *ctx) +{ + size_t i; + + if (config == NULL || fn == NULL) { + return; + } + + for (i = 0; i < config->count; i++) { + const char *section = config->entries[i].section; + size_t j; + int seen = 0; + + for (j = 0; j < i; j++) { + if (strcmp(config->entries[j].section, section) == 0) { + seen = 1; + break; + } + } + + if (!seen) { + fn(section, ctx); + } + } +} + +hybbx_result_t hybbx_config_set(hybbx_config_t *config, + const char *section, + const char *key, + const char *value) +{ + size_t i; + + if (config == NULL || section == NULL || key == NULL || value == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < config->count; i++) { + if (strcmp(config->entries[i].section, section) == 0 && + strcmp(config->entries[i].key, key) == 0) { + char *copy = hybbx_strdup(value); + + if (copy == NULL) { + return HYBBX_ERR_NOMEM; + } + free(config->entries[i].value); + config->entries[i].value = copy; + return HYBBX_OK; + } + } + + return append_entry(config, section, key, value); +} + +void hybbx_config_remove_section(hybbx_config_t *config, const char *section) +{ + size_t i; + + if (config == NULL || section == NULL) { + return; + } + + i = 0; + while (i < config->count) { + if (strcmp(config->entries[i].section, section) == 0) { + free(config->entries[i].section); + free(config->entries[i].key); + free(config->entries[i].value); + if (i + 1 < config->count) { + memmove(&config->entries[i], &config->entries[i + 1], + (config->count - i - 1) * sizeof(config->entries[i])); + } + config->count--; + } else { + i++; + } + } +} + +hybbx_result_t hybbx_config_save(const hybbx_config_t *config, + const char *path) +{ + FILE *fp; + size_t i; + const char *current = NULL; + + if (config == NULL || path == NULL) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + for (i = 0; i < config->count; i++) { + const hybbx_config_entry_t *entry = &config->entries[i]; + + if (current == NULL || strcmp(current, entry->section) != 0) { + if (current != NULL) { + fputc('\n', fp); + } + fprintf(fp, "[%s]\n", entry->section); + current = entry->section; + } + fprintf(fp, "%s = %s\n", entry->key, entry->value); + } + + fclose(fp); + return HYBBX_OK; +} diff --git a/src/core/crdop.c b/src/core/crdop.c new file mode 100644 index 0000000..67b3575 --- /dev/null +++ b/src/core/crdop.c @@ -0,0 +1,66 @@ +#include "hybbx/crdop.h" + +#include +#include + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +hybbx_crdop_radio_profile_t hybbx_crdop_profile_parse(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_CRDOP_PROFILE_AMATEUR; + } + + if (str_ieq(value, "cb") || str_ieq(value, "11m") || + str_ieq(value, "citizens_band")) { + return HYBBX_CRDOP_PROFILE_CB; + } + + return HYBBX_CRDOP_PROFILE_AMATEUR; +} + +const char *hybbx_crdop_profile_name(hybbx_crdop_radio_profile_t profile) +{ + return profile == HYBBX_CRDOP_PROFILE_CB ? "cb" : "amateur"; +} + +const char *hybbx_crdop_default_arq_bandwidth(hybbx_crdop_radio_profile_t profile) +{ + return profile == HYBBX_CRDOP_PROFILE_CB ? "500MAX" : "500MAX"; +} + +int hybbx_crdop_bandwidth_exceeds_cb(const char *arq_bandwidth) +{ + const char *p; + unsigned n = 0; + + if (arq_bandwidth == NULL || arq_bandwidth[0] == '\0') { + return 0; + } + + p = arq_bandwidth; + while (*p >= '0' && *p <= '9') { + n = n * 10u + (unsigned)(*p - '0'); + p++; + } + + return n > 1000u; +} diff --git a/src/core/crypto.c b/src/core/crypto.c new file mode 100644 index 0000000..dffcea4 --- /dev/null +++ b/src/core/crypto.c @@ -0,0 +1,302 @@ +#include "hybbx/crypto.h" + +#include "crypto_backends.h" +#include "monocypher.h" + +#include + +#define HYBBX_CRYPTO_MAX_BYTES (256u * 1024u) + +static hybbx_result_t crypto_len_ok(size_t len) +{ + if (len > HYBBX_CRYPTO_MAX_BYTES) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_random(uint8_t *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_backend_random(buf, len); +} + +const char *hybbx_crypto_alg_name(hybbx_crypto_alg_t alg) +{ + switch (alg) { + case HYBBX_CRYPTO_AES_256_GCM: + return "aes-256-gcm"; + case HYBBX_CRYPTO_XCHACHA20_POLY1305: + return "xchacha20-poly1305"; + case HYBBX_CRYPTO_X25519_AEAD: + return "x25519-xchacha20-poly1305"; + default: + return "unknown"; + } +} + +size_t hybbx_crypto_nonce_size(hybbx_crypto_alg_t alg) +{ + switch (alg) { + case HYBBX_CRYPTO_AES_256_GCM: + return HYBBX_CRYPTO_AES_GCM_NONCE; + case HYBBX_CRYPTO_XCHACHA20_POLY1305: + return HYBBX_CRYPTO_XCHACHA_NONCE; + default: + return 0; + } +} + +static hybbx_result_t derive_x25519_key(const uint8_t shared[32], + uint8_t key[32]) +{ + static const uint8_t label[] = "hybbx-x25519-aead-key-v1"; + + hybbx_backend_blake2b_keyed(key, 32, shared, 32, label, sizeof(label) - 1); + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_encrypt(hybbx_crypto_alg_t alg, + const uint8_t key[HYBBX_CRYPTO_KEY_SIZE], + const uint8_t *nonce, size_t nonce_len, + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *ciphertext, + uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]) +{ + hybbx_result_t rc; + + if (key == NULL || nonce == NULL || tag == NULL || + (plaintext_len > 0 && (plaintext == NULL || ciphertext == NULL))) { + return HYBBX_ERR_INVALID; + } + + rc = crypto_len_ok(plaintext_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (alg == HYBBX_CRYPTO_AES_256_GCM) { + if (nonce_len != HYBBX_CRYPTO_AES_GCM_NONCE) { + return HYBBX_ERR_INVALID; + } + if (hybbx_backend_aes256gcm_encrypt(key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, tag) != + 0) { + return HYBBX_ERR_IO; + } + return HYBBX_OK; + } + + if (alg == HYBBX_CRYPTO_XCHACHA20_POLY1305) { + uint8_t nonce24[HYBBX_CRYPTO_XCHACHA_NONCE]; + + if (nonce_len != HYBBX_CRYPTO_XCHACHA_NONCE) { + return HYBBX_ERR_INVALID; + } + memcpy(nonce24, nonce, sizeof(nonce24)); + hybbx_backend_chacha_lock(ciphertext, tag, key, nonce24, aad, aad_len, + plaintext, plaintext_len); + crypto_wipe(nonce24, sizeof(nonce24)); + return HYBBX_OK; + } + + return HYBBX_ERR_INVALID; +} + +hybbx_result_t hybbx_crypto_decrypt(hybbx_crypto_alg_t alg, + const uint8_t key[HYBBX_CRYPTO_KEY_SIZE], + const uint8_t *nonce, size_t nonce_len, + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *plaintext, + const uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]) +{ + hybbx_result_t rc; + int ok; + + if (key == NULL || nonce == NULL || tag == NULL || + (ciphertext_len > 0 && (ciphertext == NULL || plaintext == NULL))) { + return HYBBX_ERR_INVALID; + } + + rc = crypto_len_ok(ciphertext_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (alg == HYBBX_CRYPTO_AES_256_GCM) { + if (nonce_len != HYBBX_CRYPTO_AES_GCM_NONCE) { + return HYBBX_ERR_INVALID; + } + ok = hybbx_backend_aes256gcm_decrypt(key, nonce, aad, aad_len, + ciphertext, ciphertext_len, + plaintext, tag); + return ok == 0 ? HYBBX_OK : HYBBX_ERR_DENIED; + } + + if (alg == HYBBX_CRYPTO_XCHACHA20_POLY1305) { + uint8_t nonce24[HYBBX_CRYPTO_XCHACHA_NONCE]; + + if (nonce_len != HYBBX_CRYPTO_XCHACHA_NONCE) { + return HYBBX_ERR_INVALID; + } + memcpy(nonce24, nonce, sizeof(nonce24)); + ok = hybbx_backend_chacha_unlock(plaintext, tag, key, nonce24, aad, + aad_len, ciphertext, ciphertext_len); + crypto_wipe(nonce24, sizeof(nonce24)); + return ok == 0 ? HYBBX_OK : HYBBX_ERR_DENIED; + } + + return HYBBX_ERR_INVALID; +} + +hybbx_result_t hybbx_crypto_x25519_keypair( + uint8_t public_key[HYBBX_CRYPTO_X25519_PUBLIC_KEY], + uint8_t secret_key[HYBBX_CRYPTO_X25519_SECRET_KEY]) +{ + if (public_key == NULL || secret_key == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_crypto_random(secret_key, HYBBX_CRYPTO_X25519_SECRET_KEY) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + secret_key[0] &= 248; + secret_key[31] &= 127; + secret_key[31] |= 64; + + hybbx_backend_x25519_public_key(public_key, secret_key); + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_x25519_seal( + const uint8_t recipient_pk[HYBBX_CRYPTO_X25519_PUBLIC_KEY], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *sealed, size_t *sealed_len, size_t sealed_cap) +{ + uint8_t ep_sk[32]; + uint8_t ep_pk[32]; + uint8_t shared[32]; + uint8_t key[32]; + uint8_t nonce[HYBBX_CRYPTO_XCHACHA_NONCE]; + uint8_t *cipher; + uint8_t *tag; + size_t need; + hybbx_result_t rc; + + if (recipient_pk == NULL || sealed == NULL || sealed_len == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = crypto_len_ok(plaintext_len); + if (rc != HYBBX_OK) { + return rc; + } + + need = HYBBX_CRYPTO_X25519_SEAL_OVERHEAD + plaintext_len; + if (sealed_cap < need) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_crypto_random(ep_sk, sizeof(ep_sk)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + ep_sk[0] &= 248; + ep_sk[31] &= 127; + ep_sk[31] |= 64; + + hybbx_backend_x25519_public_key(ep_pk, ep_sk); + hybbx_backend_x25519_shared(shared, ep_sk, recipient_pk); + derive_x25519_key(shared, key); + + if (hybbx_crypto_random(nonce, sizeof(nonce)) != HYBBX_OK) { + crypto_wipe(ep_sk, sizeof(ep_sk)); + crypto_wipe(shared, sizeof(shared)); + crypto_wipe(key, sizeof(key)); + return HYBBX_ERR_IO; + } + + memcpy(sealed, ep_pk, 32); + memcpy(sealed + 32, nonce, sizeof(nonce)); + cipher = sealed + 32 + sizeof(nonce); + tag = sealed + 32 + sizeof(nonce) + plaintext_len; + + rc = hybbx_crypto_encrypt(HYBBX_CRYPTO_XCHACHA20_POLY1305, key, nonce, + sizeof(nonce), aad, aad_len, plaintext, + plaintext_len, cipher, tag); + + crypto_wipe(ep_sk, sizeof(ep_sk)); + crypto_wipe(shared, sizeof(shared)); + crypto_wipe(key, sizeof(key)); + + if (rc != HYBBX_OK) { + return rc; + } + + *sealed_len = need; + return HYBBX_OK; +} + +hybbx_result_t hybbx_crypto_x25519_open( + const uint8_t recipient_sk[HYBBX_CRYPTO_X25519_SECRET_KEY], + const uint8_t *aad, size_t aad_len, + const uint8_t *sealed, size_t sealed_len, + uint8_t *plaintext, size_t *plaintext_len, size_t plaintext_cap) +{ + const uint8_t *ep_pk; + const uint8_t *nonce; + const uint8_t *cipher; + const uint8_t *tag; + size_t cipher_len; + uint8_t shared[32]; + uint8_t key[32]; + uint8_t tag_buf[HYBBX_CRYPTO_TAG_SIZE]; + hybbx_result_t rc; + + if (recipient_sk == NULL || sealed == NULL || plaintext == NULL || + plaintext_len == NULL) { + return HYBBX_ERR_INVALID; + } + + if (sealed_len < HYBBX_CRYPTO_X25519_SEAL_OVERHEAD) { + return HYBBX_ERR_INVALID; + } + + cipher_len = sealed_len - HYBBX_CRYPTO_X25519_SEAL_OVERHEAD; + if (cipher_len > plaintext_cap) { + return HYBBX_ERR_INVALID; + } + + ep_pk = sealed; + nonce = sealed + 32; + cipher = sealed + 32 + HYBBX_CRYPTO_XCHACHA_NONCE; + tag = cipher + cipher_len; + memcpy(tag_buf, tag, sizeof(tag_buf)); + + hybbx_backend_x25519_shared(shared, recipient_sk, ep_pk); + derive_x25519_key(shared, key); + + rc = hybbx_crypto_decrypt(HYBBX_CRYPTO_XCHACHA20_POLY1305, key, nonce, + HYBBX_CRYPTO_XCHACHA_NONCE, aad, aad_len, + cipher, cipher_len, plaintext, tag_buf); + + crypto_wipe(shared, sizeof(shared)); + crypto_wipe(key, sizeof(key)); + + if (rc != HYBBX_OK) { + return rc; + } + + *plaintext_len = cipher_len; + return HYBBX_OK; +} diff --git a/src/core/crypto_backends.c b/src/core/crypto_backends.c new file mode 100644 index 0000000..da25497 --- /dev/null +++ b/src/core/crypto_backends.c @@ -0,0 +1,223 @@ +#include "crypto_backends.h" +#include "hybbx/crypto_config.h" + +#include "aes256gcm.h" +#include "monocypher.h" +#include "tinysha256.h" + +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +#if defined(HYBBX_HAVE_OPENSSL) +void hybbx_openssl_sha256_hex(const char *data, size_t len, char hex[65]); +int hybbx_openssl_aes256gcm_encrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, uint8_t tag[16]); +int hybbx_openssl_aes256gcm_decrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, const uint8_t tag[16]); +hybbx_result_t hybbx_openssl_random(uint8_t *buf, size_t len); +#endif + +#if defined(HYBBX_HAVE_LIBSODIUM) +void hybbx_libsodium_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len); +int hybbx_libsodium_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len); +void hybbx_libsodium_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]); +void hybbx_libsodium_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]); +#endif + +void hybbx_backend_sha256_hex(const char *data, size_t len, char hex[65]) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->password_hash == HYBBX_PASSWORD_HASH_OPENSSL) { + hybbx_openssl_sha256_hex(data, len, hex); + return; + } +#endif + + tinysha256_hex(data, len, hex); +} + +int hybbx_backend_aes256gcm_encrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, uint8_t tag[16]) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->aes_gcm == HYBBX_AES_GCM_OPENSSL) { + return hybbx_openssl_aes256gcm_encrypt(key, nonce, aad, aad_len, + plaintext, plaintext_len, + ciphertext, tag); + } +#endif + + return hybbx_aes256gcm_encrypt(key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, tag); +} + +int hybbx_backend_aes256gcm_decrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, const uint8_t tag[16]) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->aes_gcm == HYBBX_AES_GCM_OPENSSL) { + return hybbx_openssl_aes256gcm_decrypt(key, nonce, aad, aad_len, + ciphertext, ciphertext_len, + plaintext, tag); + } +#endif + + return hybbx_aes256gcm_decrypt(key, nonce, aad, aad_len, ciphertext, + ciphertext_len, plaintext, tag); +} + +void hybbx_backend_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->chacha == HYBBX_CHACHA_LIBSODIUM) { + hybbx_libsodium_chacha_lock(ciphertext, tag, key, nonce, aad, aad_len, + plaintext, plaintext_len); + return; + } +#endif + + crypto_aead_lock(ciphertext, tag, key, nonce, aad, aad_len, plaintext, + plaintext_len); +} + +int hybbx_backend_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->chacha == HYBBX_CHACHA_LIBSODIUM) { + return hybbx_libsodium_chacha_unlock(plaintext, tag, key, nonce, aad, + aad_len, ciphertext, + ciphertext_len); + } +#endif + + return crypto_aead_unlock(plaintext, tag, key, nonce, aad, aad_len, + ciphertext, ciphertext_len); +} + +void hybbx_backend_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->x25519 == HYBBX_X25519_LIBSODIUM) { + hybbx_libsodium_x25519_public_key(public_key, secret_key); + return; + } +#endif + + crypto_x25519_public_key(public_key, secret_key); +} + +void hybbx_backend_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]) +{ +#if defined(HYBBX_HAVE_LIBSODIUM) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->x25519 == HYBBX_X25519_LIBSODIUM) { + hybbx_libsodium_x25519_shared(shared, secret_key, peer_public_key); + return; + } +#endif + + crypto_x25519(shared, secret_key, peer_public_key); +} + +void hybbx_backend_blake2b_keyed(uint8_t *hash, size_t hash_size, + const uint8_t *message, size_t message_size, + const uint8_t *key, size_t key_size) +{ + crypto_blake2b_keyed(hash, hash_size, message, message_size, key, key_size); +} + +static hybbx_result_t system_random(uint8_t *buf, size_t len) +{ + size_t done = 0; + +#if defined(__linux__) + while (done < len) { + ssize_t n = getrandom(buf + done, len - done, 0); + + if (n < 0) { + break; + } + done += (size_t)n; + } +#endif + + if (done < len) { + int fd = open("/dev/urandom", O_RDONLY); + + if (fd < 0) { + return HYBBX_ERR_IO; + } + + while (done < len) { + ssize_t n = read(fd, buf + done, len - done); + + if (n <= 0) { + close(fd); + return HYBBX_ERR_IO; + } + done += (size_t)n; + } + + close(fd); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_backend_random(uint8_t *buf, size_t len) +{ +#if defined(HYBBX_HAVE_OPENSSL) + const hybbx_crypto_config_t *cfg = hybbx_crypto_config_get(); + + if (cfg->random == HYBBX_RANDOM_OPENSSL) { + return hybbx_openssl_random(buf, len); + } +#endif + + return system_random(buf, len); +} diff --git a/src/core/crypto_backends.h b/src/core/crypto_backends.h new file mode 100644 index 0000000..0a7f1a1 --- /dev/null +++ b/src/core/crypto_backends.h @@ -0,0 +1,62 @@ +#ifndef HYBBX_CRYPTO_BACKENDS_H +#define HYBBX_CRYPTO_BACKENDS_H + +#include "hybbx/types.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_BACKEND_SHA256_HEX_SIZE 65 +#define HYBBX_BACKEND_AES_GCM_KEY_SIZE 32 +#define HYBBX_BACKEND_AES_GCM_NONCE_SIZE 12 +#define HYBBX_BACKEND_AES_GCM_TAG_SIZE 16 +#define HYBBX_BACKEND_XCHACHA_NONCE_SIZE 24 +#define HYBBX_BACKEND_TAG_SIZE 16 + +void hybbx_backend_sha256_hex(const char *data, size_t len, char hex[65]); + +int hybbx_backend_aes256gcm_encrypt( + const uint8_t key[HYBBX_BACKEND_AES_GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_BACKEND_AES_GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, const uint8_t *plaintext, + size_t plaintext_len, uint8_t *ciphertext, + uint8_t tag[HYBBX_BACKEND_AES_GCM_TAG_SIZE]); + +int hybbx_backend_aes256gcm_decrypt( + const uint8_t key[HYBBX_BACKEND_AES_GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_BACKEND_AES_GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, const uint8_t *ciphertext, + size_t ciphertext_len, uint8_t *plaintext, + const uint8_t tag[HYBBX_BACKEND_AES_GCM_TAG_SIZE]); + +void hybbx_backend_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], + const uint8_t nonce[24], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, + size_t plaintext_len); + +int hybbx_backend_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len); + +void hybbx_backend_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]); +void hybbx_backend_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]); +void hybbx_backend_blake2b_keyed(uint8_t *hash, size_t hash_size, + const uint8_t *message, size_t message_size, + const uint8_t *key, size_t key_size); + +hybbx_result_t hybbx_backend_random(uint8_t *buf, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_CRYPTO_BACKENDS_H */ diff --git a/src/core/crypto_config.c b/src/core/crypto_config.c new file mode 100644 index 0000000..65b64ef --- /dev/null +++ b/src/core/crypto_config.c @@ -0,0 +1,247 @@ +#include "hybbx/crypto_config.h" +#include "hybbx/config.h" +#include "hybbx/log.h" + +#include +#include +#include + +static hybbx_crypto_config_t g_crypto_config; +static int g_crypto_config_ready; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static hybbx_password_hash_backend_t parse_password_hash(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "tinysha256") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_PASSWORD_HASH_TINYSHA256; + } + + if (str_ieq(value, "openssl") || str_ieq(value, "libcrypto")) { + return HYBBX_PASSWORD_HASH_OPENSSL; + } + + return HYBBX_PASSWORD_HASH_TINYSHA256; +} + +static hybbx_aes_gcm_backend_t parse_aes_gcm(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "tinyaes") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_AES_GCM_TINYAES; + } + + if (str_ieq(value, "openssl") || str_ieq(value, "libcrypto")) { + return HYBBX_AES_GCM_OPENSSL; + } + + return HYBBX_AES_GCM_TINYAES; +} + +static hybbx_chacha_backend_t parse_chacha(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "monocypher") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_CHACHA_MONOCYPHER; + } + + if (str_ieq(value, "libsodium") || str_ieq(value, "sodium")) { + return HYBBX_CHACHA_LIBSODIUM; + } + + return HYBBX_CHACHA_MONOCYPHER; +} + +static hybbx_x25519_backend_t parse_x25519(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "monocypher") || str_ieq(value, "bundled") || + str_ieq(value, "default")) { + return HYBBX_X25519_MONOCYPHER; + } + + if (str_ieq(value, "libsodium") || str_ieq(value, "sodium")) { + return HYBBX_X25519_LIBSODIUM; + } + + return HYBBX_X25519_MONOCYPHER; +} + +static hybbx_random_backend_t parse_random(const char *value) +{ + if (value == NULL || value[0] == '\0' || + str_ieq(value, "system") || str_ieq(value, "bundled") || + str_ieq(value, "default") || str_ieq(value, "getrandom") || + str_ieq(value, "urandom")) { + return HYBBX_RANDOM_SYSTEM; + } + + if (str_ieq(value, "openssl") || str_ieq(value, "libcrypto")) { + return HYBBX_RANDOM_OPENSSL; + } + + return HYBBX_RANDOM_SYSTEM; +} + +static void resolve_backends(hybbx_crypto_config_t *cfg) +{ +#if !defined(HYBBX_HAVE_OPENSSL) + if (cfg->password_hash == HYBBX_PASSWORD_HASH_OPENSSL) { + hybbx_log_warn("[crypto] password_hash=openssl requested but HyBBX was not " + "built with OpenSSL; using tinysha256"); + cfg->password_hash = HYBBX_PASSWORD_HASH_TINYSHA256; + } + if (cfg->aes_gcm == HYBBX_AES_GCM_OPENSSL) { + hybbx_log_warn("[crypto] aes_gcm=openssl requested but HyBBX was not built " + "with OpenSSL; using tinyaes"); + cfg->aes_gcm = HYBBX_AES_GCM_TINYAES; + } + if (cfg->random == HYBBX_RANDOM_OPENSSL) { + hybbx_log_warn("[crypto] random=openssl requested but HyBBX was not built " + "with OpenSSL; using system"); + cfg->random = HYBBX_RANDOM_SYSTEM; + } +#endif + +#if !defined(HYBBX_HAVE_LIBSODIUM) + if (cfg->chacha == HYBBX_CHACHA_LIBSODIUM) { + hybbx_log_warn("[crypto] chacha=libsodium requested but HyBBX was not built " + "with libsodium; using monocypher"); + cfg->chacha = HYBBX_CHACHA_MONOCYPHER; + } + if (cfg->x25519 == HYBBX_X25519_LIBSODIUM) { + hybbx_log_warn("[crypto] x25519=libsodium requested but HyBBX was not built " + "with libsodium; using monocypher"); + cfg->x25519 = HYBBX_X25519_MONOCYPHER; + } +#endif +} + +void hybbx_crypto_config_defaults(hybbx_crypto_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->password_hash = HYBBX_PASSWORD_HASH_TINYSHA256; + cfg->aes_gcm = HYBBX_AES_GCM_TINYAES; + cfg->chacha = HYBBX_CHACHA_MONOCYPHER; + cfg->x25519 = HYBBX_X25519_MONOCYPHER; + cfg->random = HYBBX_RANDOM_SYSTEM; +} + +void hybbx_crypto_config_apply(const hybbx_config_t *config) +{ + const char *value; + + hybbx_crypto_config_defaults(&g_crypto_config); + + if (config != NULL) { + value = hybbx_config_get(config, "crypto", "password_hash", NULL); + g_crypto_config.password_hash = parse_password_hash(value); + + value = hybbx_config_get(config, "crypto", "aes_gcm", NULL); + g_crypto_config.aes_gcm = parse_aes_gcm(value); + + value = hybbx_config_get(config, "crypto", "chacha", NULL); + g_crypto_config.chacha = parse_chacha(value); + + value = hybbx_config_get(config, "crypto", "x25519", NULL); + g_crypto_config.x25519 = parse_x25519(value); + + value = hybbx_config_get(config, "crypto", "random", NULL); + g_crypto_config.random = parse_random(value); + } + + resolve_backends(&g_crypto_config); + g_crypto_config_ready = 1; + + hybbx_log_info("[crypto] password_hash=%s aes_gcm=%s chacha=%s x25519=%s random=%s", + hybbx_password_hash_backend_name(g_crypto_config.password_hash), + hybbx_aes_gcm_backend_name(g_crypto_config.aes_gcm), + hybbx_chacha_backend_name(g_crypto_config.chacha), + hybbx_x25519_backend_name(g_crypto_config.x25519), + hybbx_random_backend_name(g_crypto_config.random)); +} + +const hybbx_crypto_config_t *hybbx_crypto_config_get(void) +{ + if (!g_crypto_config_ready) { + hybbx_crypto_config_defaults(&g_crypto_config); + g_crypto_config_ready = 1; + } + + return &g_crypto_config; +} + +const char *hybbx_password_hash_backend_name(hybbx_password_hash_backend_t b) +{ + switch (b) { + case HYBBX_PASSWORD_HASH_OPENSSL: + return "openssl"; + default: + return "tinysha256"; + } +} + +const char *hybbx_aes_gcm_backend_name(hybbx_aes_gcm_backend_t b) +{ + switch (b) { + case HYBBX_AES_GCM_OPENSSL: + return "openssl"; + default: + return "tinyaes"; + } +} + +const char *hybbx_chacha_backend_name(hybbx_chacha_backend_t b) +{ + switch (b) { + case HYBBX_CHACHA_LIBSODIUM: + return "libsodium"; + default: + return "monocypher"; + } +} + +const char *hybbx_x25519_backend_name(hybbx_x25519_backend_t b) +{ + switch (b) { + case HYBBX_X25519_LIBSODIUM: + return "libsodium"; + default: + return "monocypher"; + } +} + +const char *hybbx_random_backend_name(hybbx_random_backend_t b) +{ + switch (b) { + case HYBBX_RANDOM_OPENSSL: + return "openssl"; + default: + return "system"; + } +} diff --git a/src/core/crypto_libsodium.c b/src/core/crypto_libsodium.c new file mode 100644 index 0000000..85cf537 --- /dev/null +++ b/src/core/crypto_libsodium.c @@ -0,0 +1,111 @@ +#include "hybbx/types.h" + +#include + +#include +#include + +static int sodium_ready; + +static int ensure_sodium(void) +{ + if (!sodium_ready) { + if (sodium_init() < 0) { + return -1; + } + sodium_ready = 1; + } + + return 0; +} + +void hybbx_libsodium_chacha_lock(uint8_t *ciphertext, uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len) +{ + unsigned long long out_len = 0; + uint8_t *packed; + size_t packed_cap; + + if (ensure_sodium() != 0) { + return; + } + + packed_cap = plaintext_len + + crypto_aead_xchacha20poly1305_ietf_ABYTES; + packed = malloc(packed_cap); + if (packed == NULL) { + return; + } + + if (crypto_aead_xchacha20poly1305_ietf_encrypt( + packed, &out_len, plaintext, plaintext_len, aad, aad_len, NULL, + nonce, key) != 0) { + free(packed); + return; + } + + if (plaintext_len > 0 && ciphertext != NULL) { + memcpy(ciphertext, packed, plaintext_len); + } + memcpy(tag, packed + plaintext_len, + crypto_aead_xchacha20poly1305_ietf_ABYTES); + free(packed); +} + +int hybbx_libsodium_chacha_unlock(uint8_t *plaintext, const uint8_t tag[16], + const uint8_t key[32], const uint8_t nonce[24], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len) +{ + unsigned long long pt_len = 0; + uint8_t *packed; + size_t packed_len; + int rc; + + if (ensure_sodium() != 0) { + return -1; + } + + packed_len = ciphertext_len + + crypto_aead_xchacha20poly1305_ietf_ABYTES; + packed = malloc(packed_len); + if (packed == NULL) { + return -1; + } + + if (ciphertext_len > 0 && ciphertext != NULL) { + memcpy(packed, ciphertext, ciphertext_len); + } + memcpy(packed + ciphertext_len, tag, + crypto_aead_xchacha20poly1305_ietf_ABYTES); + + rc = crypto_aead_xchacha20poly1305_ietf_decrypt( + plaintext, &pt_len, NULL, packed, packed_len, aad, aad_len, nonce, + key); + + free(packed); + return rc; +} + +void hybbx_libsodium_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]) +{ + if (ensure_sodium() != 0) { + return; + } + + crypto_scalarmult_base(public_key, secret_key); +} + +void hybbx_libsodium_x25519_shared(uint8_t shared[32], + const uint8_t secret_key[32], + const uint8_t peer_public_key[32]) +{ + if (ensure_sodium() != 0) { + return; + } + + crypto_scalarmult(shared, secret_key, peer_public_key); +} diff --git a/src/core/crypto_openssl.c b/src/core/crypto_openssl.c new file mode 100644 index 0000000..551ea83 --- /dev/null +++ b/src/core/crypto_openssl.c @@ -0,0 +1,142 @@ +#include "hybbx/types.h" + +#include +#include + +#include +#include + +void hybbx_openssl_sha256_hex(const char *data, size_t len, char hex[65]) +{ + unsigned char digest[32]; + unsigned int digest_len = sizeof(digest); + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + if (ctx == NULL) { + hex[0] = '\0'; + return; + } + + if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1 || + EVP_DigestUpdate(ctx, data, len) != 1 || + EVP_DigestFinal_ex(ctx, digest, &digest_len) != 1) { + EVP_MD_CTX_free(ctx); + hex[0] = '\0'; + return; + } + + EVP_MD_CTX_free(ctx); + + for (size_t i = 0; i < digest_len; i++) { + snprintf(hex + i * 2, 3, "%02x", digest[i]); + } +} + +static int openssl_gcm_crypt(int encrypt, const uint8_t key[32], + const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *in, size_t in_len, + uint8_t *out, const uint8_t tag_in[16], + uint8_t tag_out[16]) +{ + EVP_CIPHER_CTX *ctx = NULL; + int out_len = 0; + int total = 0; + int rc = -1; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) { + return -1; + } + + if (EVP_CipherInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL, encrypt) != + 1 || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, NULL) != 1 || + EVP_CipherInit_ex(ctx, NULL, NULL, key, nonce, encrypt) != 1) { + goto done; + } + + if (aad_len > 0 && aad != NULL) { + if (EVP_CipherUpdate(ctx, NULL, &out_len, aad, (int)aad_len) != 1) { + goto done; + } + } + + if (in_len > 0) { + if (EVP_CipherUpdate(ctx, out, &out_len, in, (int)in_len) != 1) { + goto done; + } + total = out_len; + } + + if (encrypt) { + if (EVP_EncryptFinal_ex(ctx, out + total, &out_len) != 1) { + goto done; + } + if (tag_out == NULL || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag_out) != 1) { + goto done; + } + rc = 0; + goto done; + } + + if (tag_in == NULL || + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, + (void *)(uintptr_t)tag_in) != 1) { + goto done; + } + + if (EVP_DecryptFinal_ex(ctx, out + total, &out_len) != 1) { + goto done; + } + + rc = 0; +done: + EVP_CIPHER_CTX_free(ctx); + return rc; +} + +int hybbx_openssl_aes256gcm_encrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, uint8_t tag[16]) +{ + if (key == NULL || nonce == NULL || tag == NULL) { + return -1; + } + if (plaintext_len > 0 && (plaintext == NULL || ciphertext == NULL)) { + return -1; + } + + return openssl_gcm_crypt(1, key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, NULL, tag); +} + +int hybbx_openssl_aes256gcm_decrypt( + const uint8_t key[32], const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, const uint8_t tag[16]) +{ + if (key == NULL || nonce == NULL || tag == NULL) { + return -1; + } + if (ciphertext_len > 0 && (ciphertext == NULL || plaintext == NULL)) { + return -1; + } + + return openssl_gcm_crypt(0, key, nonce, aad, aad_len, ciphertext, + ciphertext_len, plaintext, tag, NULL); +} + +hybbx_result_t hybbx_openssl_random(uint8_t *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (RAND_bytes(buf, (int)len) == 1) { + return HYBBX_OK; + } + + return HYBBX_ERR_IO; +} diff --git a/src/core/daemon_wrap.c b/src/core/daemon_wrap.c new file mode 100644 index 0000000..04c990a --- /dev/null +++ b/src/core/daemon_wrap.c @@ -0,0 +1,320 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/daemon_wrap.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include +#include +#include +#include + +void hybbx_daemon_launch_opts_defaults(hybbx_daemon_launch_opts_t *opts) +{ + if (opts == NULL) { + return; + } + + memset(opts, 0, sizeof(*opts)); + hybbx_strlcpy(opts->session, HYBBX_DAEMON_DEFAULT_SESSION, + sizeof(opts->session)); +} + +static int path_executable(const char *path) +{ + return path != NULL && path[0] != '\0' && access(path, X_OK) == 0; +} + +static int find_in_path(const char *name, char *out, size_t out_len) +{ + const char *path_env; + char copy[HYBBX_PATH_MAX]; + char *save = NULL; + char *dir; + + if (name == NULL || out == NULL || out_len == 0) { + return 0; + } + + path_env = getenv("PATH"); + if (path_env == NULL || path_env[0] == '\0') { + return 0; + } + + snprintf(copy, sizeof(copy), "%s", path_env); + for (dir = strtok_r(copy, ":", &save); dir != NULL; + dir = strtok_r(NULL, ":", &save)) { + if (hybbx_path_join(out, out_len, dir, name) != HYBBX_OK) { + continue; + } + if (path_executable(out)) { + return 1; + } + } + + return 0; +} + +static int wrapped_child(void) +{ + const char *env = getenv(HYBBX_DAEMON_WRAP_ENV); + + return env != NULL && env[0] == '1'; +} + +static int in_screen(void) +{ + const char *sty = getenv("STY"); + + return sty != NULL && sty[0] != '\0'; +} + +static int in_tmux(void) +{ + const char *tmux = getenv("TMUX"); + + return tmux != NULL && tmux[0] != '\0'; +} + +static int arg_is_launch_flag(const char *arg) +{ + if (arg == NULL) { + return 0; + } + + return strcmp(arg, "-f") == 0 || strcmp(arg, "--foreground") == 0 || + strcmp(arg, "--attach") == 0 || strcmp(arg, "--screen") == 0 || + strcmp(arg, "--tmux") == 0; +} + +static int build_inner_argv(char **out, int out_max, + const char *binary, + int argc, char **argv) +{ + int outc = 0; + int i; + + if (out == NULL || out_max < 2 || binary == NULL) { + return -1; + } + + out[outc++] = (char *)binary; + + if (!wrapped_child()) { + out[outc++] = "-f"; + } + + for (i = 1; i < argc && outc < out_max - 1; i++) { + if (strcmp(argv[i], "--screen") == 0 || strcmp(argv[i], "--tmux") == 0) { + if (i + 1 < argc && argv[i + 1][0] != '-' && + !arg_is_launch_flag(argv[i + 1])) { + i++; + } + continue; + } + if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--foreground") == 0 || + strcmp(argv[i], "--attach") == 0) { + continue; + } + + out[outc++] = argv[i]; + } + + out[outc] = NULL; + return outc; +} + +static hybbx_result_t exec_screen_attach(const char *session) +{ + char screen_path[HYBBX_PATH_MAX]; + char *args[8]; + int n = 0; + + if (!find_in_path("screen", screen_path, sizeof(screen_path))) { + fprintf(stderr, "hybbxd: GNU screen not found in PATH\n"); + return HYBBX_ERR_IO; + } + + args[n++] = screen_path; + args[n++] = "-r"; + args[n++] = (char *)session; + args[n++] = NULL; + + execv(screen_path, args); + fprintf(stderr, "hybbxd: screen attach failed\n"); + return HYBBX_ERR_IO; +} + +static hybbx_result_t exec_tmux_attach(const char *session) +{ + char tmux_path[HYBBX_PATH_MAX]; + char *args[8]; + int n = 0; + + if (!find_in_path("tmux", tmux_path, sizeof(tmux_path))) { + fprintf(stderr, "hybbxd: tmux not found in PATH\n"); + return HYBBX_ERR_IO; + } + + args[n++] = tmux_path; + args[n++] = "attach"; + args[n++] = "-t"; + args[n++] = (char *)session; + args[n++] = NULL; + + execv(tmux_path, args); + fprintf(stderr, "hybbxd: tmux attach failed\n"); + return HYBBX_ERR_IO; +} + +static hybbx_result_t spawn_screen(const char *session, const char *binary, + int argc, char **argv) +{ + char screen_path[HYBBX_PATH_MAX]; + char *inner[HYBBX_CMD_TOKEN_MAX + 4]; + char wrap_env[64]; + int innerc; + char *args[HYBBX_CMD_TOKEN_MAX + 8]; + int n = 0; + + if (!find_in_path("screen", screen_path, sizeof(screen_path))) { + fprintf(stderr, "hybbxd: GNU screen not found in PATH\n"); + return HYBBX_ERR_IO; + } + + innerc = build_inner_argv(inner, (int)(sizeof(inner) / sizeof(inner[0])), + binary, argc, argv); + if (innerc < 0) { + return HYBBX_ERR_INVALID; + } + + snprintf(wrap_env, sizeof(wrap_env), "%s=1", HYBBX_DAEMON_WRAP_ENV); + + args[n++] = screen_path; + args[n++] = "-dmS"; + args[n++] = (char *)session; + args[n++] = "env"; + args[n++] = wrap_env; + { + int i; + + for (i = 0; i < innerc; i++) { + args[n++] = inner[i]; + } + } + args[n++] = NULL; + + execv(screen_path, args); + fprintf(stderr, "hybbxd: screen spawn failed\n"); + return HYBBX_ERR_IO; +} + +static hybbx_result_t spawn_tmux(const char *session, const char *binary, + int argc, char **argv) +{ + char tmux_path[HYBBX_PATH_MAX]; + char *inner[HYBBX_CMD_TOKEN_MAX + 4]; + char wrap_env[64]; + int innerc; + char *args[HYBBX_CMD_TOKEN_MAX + 8]; + int n = 0; + + if (!find_in_path("tmux", tmux_path, sizeof(tmux_path))) { + fprintf(stderr, "hybbxd: tmux not found in PATH\n"); + return HYBBX_ERR_IO; + } + + innerc = build_inner_argv(inner, (int)(sizeof(inner) / sizeof(inner[0])), + binary, argc, argv); + if (innerc < 0) { + return HYBBX_ERR_INVALID; + } + + snprintf(wrap_env, sizeof(wrap_env), "%s=1", HYBBX_DAEMON_WRAP_ENV); + + args[n++] = tmux_path; + args[n++] = "new-session"; + args[n++] = "-d"; + args[n++] = "-s"; + args[n++] = (char *)session; + args[n++] = "env"; + args[n++] = wrap_env; + { + int i; + + for (i = 0; i < innerc; i++) { + args[n++] = inner[i]; + } + } + args[n++] = NULL; + + execv(tmux_path, args); + fprintf(stderr, "hybbxd: tmux spawn failed\n"); + return HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_daemon_apply_launch_opts(const hybbx_daemon_launch_opts_t *opts, + const char *binary_path, + int argc, char **argv) +{ + const char *binary = binary_path; + + if (opts == NULL) { + return HYBBX_ERR_INVALID; + } + + if (binary == NULL || binary[0] == '\0') { + binary = HYBBX_DAEMON_BINARY; + } + + if (opts->use_screen && opts->use_tmux) { + fprintf(stderr, "hybbxd: use --screen or --tmux, not both\n"); + return HYBBX_ERR_INVALID; + } + + if (opts->attach) { + if (opts->use_screen) { + return exec_screen_attach(opts->session); + } + if (opts->use_tmux) { + return exec_tmux_attach(opts->session); + } + + fprintf(stderr, "hybbxd: --attach requires --screen or --tmux\n"); + return HYBBX_ERR_INVALID; + } + + if (wrapped_child() || opts->foreground) { + return HYBBX_OK; + } + + if (opts->use_screen) { + if (in_screen()) { + return HYBBX_OK; + } + + if (spawn_screen(opts->session, binary, argc, argv) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + printf("hybbxd: started in screen session '%s'\n", opts->session); + printf("attach: screen -r %s\n", opts->session); + exit(EXIT_SUCCESS); + } + + if (opts->use_tmux) { + if (in_tmux()) { + return HYBBX_OK; + } + + if (spawn_tmux(opts->session, binary, argc, argv) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + printf("hybbxd: started in tmux session '%s'\n", opts->session); + printf("attach: tmux attach -t %s\n", opts->session); + exit(EXIT_SUCCESS); + } + + return HYBBX_OK; +} diff --git a/src/core/instance.c b/src/core/instance.c new file mode 100644 index 0000000..4640678 --- /dev/null +++ b/src/core/instance.c @@ -0,0 +1,272 @@ +#include "hybbx/instance.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include + +static int g_instance_standalone; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +#if defined(__GNUC__) || defined(__clang__) +__attribute__((weak)) +#endif +hybbx_instance_role_t hybbx_instance_role(void) +{ + /* Overridden by instance_role_{main,secondary,proxy}.c on each binary. */ + return HYBBX_INSTANCE_MAIN; +} + +const char *hybbx_instance_binary_name(void) +{ + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_SECONDARY: + return "hybbxsd"; + case HYBBX_INSTANCE_PROXY: + return "hybbxpd"; + case HYBBX_INSTANCE_MAIN: + default: + return "hybbxd"; + } +} + +const char *hybbx_instance_role_name(void) +{ + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_SECONDARY: + return "Secondary"; + case HYBBX_INSTANCE_PROXY: + return "Proxy"; + case HYBBX_INSTANCE_MAIN: + default: + return "Main"; + } +} + +int hybbx_instance_offers_user_bbx(void) +{ + return hybbx_instance_role() == HYBBX_INSTANCE_MAIN; +} + +void hybbx_instance_set_standalone(int enabled) +{ + g_instance_standalone = enabled != 0; +} + +int hybbx_instance_standalone(void) +{ + return g_instance_standalone; +} + +static int instance_main_standalone(void) +{ + return hybbx_instance_role() == HYBBX_INSTANCE_MAIN && + hybbx_instance_standalone(); +} + +int hybbx_instance_plugin_allowed(const char *plugin_name) +{ + hybbx_instance_role_t role; + + if (plugin_name == NULL || plugin_name[0] == '\0') { + return 0; + } + + role = hybbx_instance_role(); + + if (instance_main_standalone()) { + if (str_ieq(plugin_name, "circuit")) { + return 1; + } + if (str_ieq(plugin_name, "mains_proxy")) { + return 1; + } + return 1; + } + + /* Split layout: packet_radio stays in HyBBX only via MAX25 prep on Secondary/Proxy; + * hub Main uses circuit peers instead. */ + if (str_ieq(plugin_name, "packet_radio")) { + return role != HYBBX_INSTANCE_MAIN; + } + + switch (role) { + case HYBBX_INSTANCE_MAIN: + if (str_ieq(plugin_name, "baycom") || + str_ieq(plugin_name, "ardop") || + str_ieq(plugin_name, "crdop")) { + return 0; + } + if (str_ieq(plugin_name, "telnet") || str_ieq(plugin_name, "ssh")) { + return 0; + } + return 1; + + case HYBBX_INSTANCE_PROXY: + if (str_ieq(plugin_name, "websocket")) { + return 0; + } + return 1; + + case HYBBX_INSTANCE_SECONDARY: + if (str_ieq(plugin_name, "telnet") || + str_ieq(plugin_name, "ssh") || + str_ieq(plugin_name, "websocket")) { + return 0; + } + return 1; + + default: + return 0; + } +} + +hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks) +{ + hybbx_instance_role_t role; + + if (networks == NULL) { + return HYBBX_ERR_INVALID; + } + + role = hybbx_instance_role(); + + if (instance_main_standalone()) { + hybbx_log_info("[instance] standalone=yes — %s keeps INI transports " + "(local RF via MAX25 prep when configured)", + hybbx_instance_binary_name()); + hybbx_log_info("[instance] binary=%s role=%s standalone=yes user_bbx=%s " + "telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s " + "websocket=%s circuit=%s mains_proxy=%s", + hybbx_instance_binary_name(), + hybbx_instance_role_name(), + hybbx_instance_offers_user_bbx() ? "yes" : "no", + hybbx_bool_to_string(networks->telnet), + hybbx_bool_to_string(networks->ssh), + hybbx_bool_to_string(networks->ax25), + hybbx_bool_to_string(networks->baycom), + hybbx_bool_to_string(networks->ardop), + hybbx_bool_to_string(networks->crdop), + hybbx_bool_to_string(networks->websocket), + hybbx_bool_to_string(networks->circuit), + hybbx_bool_to_string(networks->mains_proxy)); + return HYBBX_OK; + } + + if (networks->ax25 && role == HYBBX_INSTANCE_MAIN) { + hybbx_log_warn("[instance] %s: ax25/packet_radio on hub Main — forcing off " + "(use standalone=yes or Secondary RF host)", + hybbx_instance_binary_name()); + networks->ax25 = 0; + } + + switch (role) { + case HYBBX_INSTANCE_MAIN: + if (networks->baycom || networks->ardop || networks->crdop) { + hybbx_log_warn("[instance] hybbxd: RF adapters forbidden on Main " + "— forcing baycom/ardop/crdop off (use hybbxpd + MAX25)"); + networks->baycom = 0; + networks->ardop = 0; + networks->crdop = 0; + } + if (networks->ssh) { + hybbx_log_warn("[instance] hybbxd: SSH user path forbidden — " + "WebSocket/reverse-proxy only; forcing ssh=no"); + networks->ssh = 0; + } + if (networks->telnet) { + hybbx_log_warn("[instance] hybbxd: telnet user path forbidden — " + "WebSocket/reverse-proxy only; forcing telnet=no"); + networks->telnet = 0; + } + /* WebSocket is the sole remote user path. */ + if (!networks->websocket) { + hybbx_log_warn("[instance] hybbxd: enabling websocket " + "(required user path)"); + networks->websocket = 1; + } + /* Circuit hub for Proxy attach. */ + if (!networks->circuit) { + hybbx_log_warn("[instance] hybbxd: enabling circuit hub " + "(Proxy in/out)"); + networks->circuit = 1; + } + break; + + case HYBBX_INSTANCE_SECONDARY: + if (networks->telnet) { + hybbx_log_warn("[instance] hybbxsd: no user telnet — forcing off"); + networks->telnet = 0; + } + if (networks->websocket) { + hybbx_log_warn("[instance] hybbxsd: no user WebSocket — forcing off"); + networks->websocket = 0; + } + if (networks->ssh) { + hybbx_log_warn("[instance] hybbxsd: no user SSH — forcing off"); + networks->ssh = 0; + } + /* Peer other Mains — mains_proxy required. */ + if (!networks->mains_proxy) { + hybbx_log_warn("[instance] hybbxsd: enabling mains_proxy " + "(Secondary = other-Main peers)"); + networks->mains_proxy = 1; + } + break; + + case HYBBX_INSTANCE_PROXY: + if (networks->websocket) { + hybbx_log_warn("[instance] hybbxpd: WebSocket forbidden — forcing off"); + networks->websocket = 0; + } + if (networks->baycom) { + hybbx_log_warn("[instance] hybbxpd: baycom enabled — prefer MAX25 " + "relay/gateway/digipeater for hyBBX RF"); + } + if (!networks->circuit) { + hybbx_log_warn("[instance] hybbxpd: enabling circuit " + "(attach toward Main)"); + networks->circuit = 1; + } + break; + + default: + return HYBBX_ERR_INVALID; + } + + hybbx_log_info("[instance] binary=%s role=%s user_bbx=%s " + "telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s crdop=%s " + "websocket=%s circuit=%s mains_proxy=%s", + hybbx_instance_binary_name(), + hybbx_instance_role_name(), + hybbx_instance_offers_user_bbx() ? "yes" : "no", + hybbx_bool_to_string(networks->telnet), + hybbx_bool_to_string(networks->ssh), + hybbx_bool_to_string(networks->ax25), + hybbx_bool_to_string(networks->baycom), + hybbx_bool_to_string(networks->ardop), + hybbx_bool_to_string(networks->crdop), + hybbx_bool_to_string(networks->websocket), + hybbx_bool_to_string(networks->circuit), + hybbx_bool_to_string(networks->mains_proxy)); + + return HYBBX_OK; +} diff --git a/src/core/link.c b/src/core/link.c new file mode 100644 index 0000000..b9a38cd --- /dev/null +++ b/src/core/link.c @@ -0,0 +1,427 @@ +/* + * Link/repeater edge registry: data/links/.ini, [link.], stale prune. + */ +#include "hybbx/link.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include + +static char *link_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static const char *LINK_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +void hybbx_link_generate_code(char *out, size_t out_cap) +{ + static unsigned seed; + size_t i; + size_t n; + + if (out == NULL || out_cap < 2) { + return; + } + + if (seed == 0) { + seed = (unsigned)time(NULL) ^ 0x48594242u; + } + + n = out_cap - 1; + if (n > HYBBX_LINK_CODE_MAX - 1) { + n = HYBBX_LINK_CODE_MAX - 1; + } + + for (i = 0; i < n; i++) { + seed = seed * 1103515245u + 12345u; + out[i] = LINK_CODE_CHARS[(seed >> 16) % 32]; + } + out[n] = '\0'; +} + +void hybbx_link_registry_init(hybbx_link_registry_t *reg, + const char *data_dir, + const char *config_path, + unsigned stale_days) +{ + if (reg == NULL) { + return; + } + + memset(reg, 0, sizeof(*reg)); + if (data_dir != NULL && data_dir[0] != '\0') { + snprintf(reg->dir, sizeof(reg->dir), "%s/links", data_dir); + } + if (config_path != NULL) { + hybbx_strlcpy(reg->config_path, config_path, sizeof(reg->config_path)); + } + reg->stale_days = stale_days > 0 ? stale_days : HYBBX_LINK_STALE_DAYS; +} + +static hybbx_result_t link_shard_path(const hybbx_link_registry_t *reg, + const char *id, + char *out, size_t out_cap) +{ + if (reg == NULL || id == NULL || id[0] == '\0' || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (snprintf(out, out_cap, "%s/%s.ini", reg->dir, id) >= (int)out_cap) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static hybbx_result_t read_link_last_seen(const char *path, time_t *last_seen) +{ + FILE *fp; + char line[256]; + time_t value = 0; + + if (path == NULL || last_seen == NULL) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + *last_seen = 0; + return HYBBX_OK; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "last_seen=", 10) == 0) { + value = (time_t)strtol(line + 10, NULL, 10); + } + } + + fclose(fp); + *last_seen = value; + return HYBBX_OK; +} + +static hybbx_result_t write_link_shard(const char *path, + const char *id, + const char *role, + const char *link_code, + time_t now, + int is_new) +{ + FILE *fp; + time_t created = now; + char existing_code[HYBBX_LINK_CODE_MAX]; + + existing_code[0] = '\0'; + if (!is_new) { + FILE *old = fopen(path, "r"); + char line[256]; + + if (old != NULL) { + while (fgets(line, sizeof(line), old) != NULL) { + if (strncmp(line, "created=", 8) == 0) { + created = (time_t)strtol(line + 8, NULL, 10); + } else if (strncmp(line, "link_code=", 10) == 0) { + size_t n = strcspn(line + 10, "\r\n"); + if (n >= sizeof(existing_code)) { + n = sizeof(existing_code) - 1; + } + memcpy(existing_code, line + 10, n); + existing_code[n] = '\0'; + } + } + fclose(old); + } + } + + if (link_code != NULL && link_code[0] != '\0') { + hybbx_strlcpy(existing_code, link_code, sizeof(existing_code)); + } else if (existing_code[0] == '\0') { + hybbx_link_generate_code(existing_code, sizeof(existing_code)); + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "id=%s\n", id); + fprintf(fp, "role=%s\n", role != NULL && role[0] != '\0' ? role : "link"); + fprintf(fp, "link_code=%s\n", existing_code); + fprintf(fp, "created=%ld\n", (long)created); + fprintf(fp, "last_seen=%ld\n", (long)now); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t config_upsert_link_section(const char *config_path, + const char *id, + const char *role, + const char *link_code, + time_t last_seen) +{ + hybbx_config_t cfg; + char section[HYBBX_CONFIG_SECTION_MAX]; + char value[64]; + size_t i; + hybbx_result_t rc; + int found_role = 0; + int found_code = 0; + int found_seen = 0; + + if (config_path == NULL || config_path[0] == '\0' || id == NULL) { + return HYBBX_OK; + } + + rc = hybbx_config_load(&cfg, config_path); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(section, sizeof(section), "link.%s", id); + snprintf(value, sizeof(value), "%ld", (long)last_seen); + + for (i = 0; i < cfg.count; i++) { + if (cfg.entries[i].section == NULL || cfg.entries[i].key == NULL) { + continue; + } + if (strcmp(cfg.entries[i].section, section) != 0) { + continue; + } + if (strcmp(cfg.entries[i].key, "role") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(role != NULL ? role : "link"); + found_role = 1; + } else if (strcmp(cfg.entries[i].key, "link_code") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(link_code != NULL ? link_code : ""); + found_code = 1; + } else if (strcmp(cfg.entries[i].key, "last_seen") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(value); + found_seen = 1; + } + } + + if (!found_role) { + rc = hybbx_config_set(&cfg, section, "role", + role != NULL ? role : "link"); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + if (!found_code && link_code != NULL) { + rc = hybbx_config_set(&cfg, section, "link_code", link_code); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + if (!found_seen) { + rc = hybbx_config_set(&cfg, section, "last_seen", value); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + + rc = hybbx_config_save(&cfg, config_path); + hybbx_config_free(&cfg); + return rc; +} + +hybbx_result_t hybbx_link_registry_touch(hybbx_link_registry_t *reg, + const char *id, + const char *role, + char *link_code_out, + size_t link_code_cap) +{ + char path[HYBBX_PATH_MAX]; + char code[HYBBX_LINK_CODE_MAX]; + time_t now = time(NULL); + struct stat st; + hybbx_result_t rc; + int is_new; + + if (reg == NULL || id == NULL || id[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (reg->dir[0] != '\0') { + mkdir_p(reg->dir); + } + + rc = link_shard_path(reg, id, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + is_new = stat(path, &st) != 0; + code[0] = '\0'; + if (!is_new) { + FILE *fp = fopen(path, "r"); + char line[256]; + + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "link_code=", 10) == 0) { + size_t n = strcspn(line + 10, "\r\n"); + if (n >= sizeof(code)) { + n = sizeof(code) - 1; + } + memcpy(code, line + 10, n); + code[n] = '\0'; + } + } + fclose(fp); + } + } + if (code[0] == '\0') { + hybbx_link_generate_code(code, sizeof(code)); + } + + rc = write_link_shard(path, id, role, code, now, is_new); + if (rc != HYBBX_OK) { + return rc; + } + + if (link_code_out != NULL && link_code_cap > 0) { + hybbx_strlcpy(link_code_out, code, link_code_cap); + } + + return config_upsert_link_section(reg->config_path, id, role, code, now); +} + +static hybbx_result_t remove_link_files(const hybbx_link_registry_t *reg, + const char *id) +{ + char path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + rc = link_shard_path(reg, id, path, sizeof(path)); + if (rc == HYBBX_OK) { + remove(path); + } + + if (reg->config_path[0] != '\0') { + hybbx_config_t cfg; + char section[HYBBX_CONFIG_SECTION_MAX]; + + snprintf(section, sizeof(section), "link.%s", id); + if (hybbx_config_load(&cfg, reg->config_path) == HYBBX_OK) { + hybbx_config_remove_section(&cfg, section); + hybbx_config_save(&cfg, reg->config_path); + hybbx_config_free(&cfg); + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_link_registry_prune(hybbx_link_registry_t *reg) +{ + DIR *dir; + struct dirent *ent; + time_t now = time(NULL); + time_t cutoff; + char id[HYBBX_LINK_ID_MAX]; + + if (reg == NULL || reg->dir[0] == '\0') { + return HYBBX_OK; + } + + cutoff = now - (time_t)reg->stale_days * 24 * 60 * 60; + + dir = opendir(reg->dir); + if (dir == NULL) { + return HYBBX_OK; + } + + while ((ent = readdir(dir)) != NULL) { + const char *name = ent->d_name; + size_t nlen = strlen(name); + time_t last_seen; + char path[HYBBX_PATH_MAX]; + + if (nlen < 5 || strcmp(name + nlen - 4, ".ini") != 0) { + continue; + } + + nlen -= 4; + if (nlen >= sizeof(id)) { + continue; + } + memcpy(id, name, nlen); + id[nlen] = '\0'; + + if (snprintf(path, sizeof(path), "%s/%s", reg->dir, name) >= (int)sizeof(path)) { + continue; + } + + if (read_link_last_seen(path, &last_seen) != HYBBX_OK) { + continue; + } + + if (last_seen > 0 && last_seen < cutoff) { + hybbx_log_info("[links] removing stale link '%s' (last auth %ld days ago)", + id, (long)((now - last_seen) / (24 * 60 * 60))); + remove_link_files(reg, id); + } + } + + closedir(dir); + return HYBBX_OK; +} diff --git a/src/core/log.c b/src/core/log.c new file mode 100644 index 0000000..7812c00 --- /dev/null +++ b/src/core/log.c @@ -0,0 +1,694 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/log.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif +#include "hybbx/limits.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_LOG_LINE_MAX 1024u + +/* + * Weekly HyBBX file log (replaces stuck month-keyed "daily" open handle). + * + * Naming: YYYYMMDD-week-hybbx.log + * YYYYMMDD = ISO week start (Monday, local time). One file per 7-day week. + * + * Cycle (forever): + * Days 1–7 → append to the current week file. + * Day 8 → switch to the new week file (new Monday), then pack every + * closed/legacy packable log under [log] dir into + * logs/arc/-week-hybbx.tar.bz2 (bzip2) and delete + * those sources only after tar succeeds. + * Pack batches of up to 7 members per archive (oldest first); repeat until + * fewer than 1 packable remain. Legacy daily names (YYYYMMDD-hybbx.log) + * are packable and are swept on day-8 switch (and on startup when ≥7). + * + * security.log is never packed or deleted here. + */ + +#define HYBBX_LOG_ARC_SUBDIR "arc" +#define HYBBX_LOG_PACK_BATCH 7 +#define HYBBX_LOG_PACKABLE_MAX 64 + +static hybbx_log_config_t g_log_config; +static int g_log_ready; +static FILE *g_log_file; +/* ISO-Monday yyyymmdd of the file currently open (0 = none). */ +static int g_log_open_week_yyyymmdd; +static char g_log_open_name[64]; +static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + hybbx_strlcpy(buf, path, sizeof(buf)); + len = strlen(buf); + while (len > 0 && buf[len - 1] == '/') { + buf[--len] = '\0'; + } + + for (i = 1; i < len; i++) { + if (buf[i] != '/') { + continue; + } + buf[i] = '\0'; + if (buf[0] != '\0' && mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +void hybbx_log_config_defaults(hybbx_log_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->enabled = 1; + cfg->dir[0] = '\0'; + cfg->level = HYBBX_LOG_WARN; +} + +hybbx_log_level_t hybbx_log_parse_level(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_LOG_WARN; + } + + if (strcasecmp(value, "debug") == 0) { + return HYBBX_LOG_DEBUG; + } + if (strcasecmp(value, "stats") == 0) { + return HYBBX_LOG_STATS; + } + if (strcasecmp(value, "info") == 0) { + return HYBBX_LOG_INFO; + } + if (strcasecmp(value, "warn") == 0) { + return HYBBX_LOG_WARN; + } + + return HYBBX_LOG_WARN; +} + +const char *hybbx_log_level_name(hybbx_log_level_t level) +{ + switch (level) { + case HYBBX_LOG_DEBUG: + return "debug"; + case HYBBX_LOG_STATS: + return "stats"; + case HYBBX_LOG_INFO: + return "info"; + case HYBBX_LOG_WARN: + return "warn"; + default: + return "?"; + } +} + +int hybbx_log_level_visible(hybbx_log_level_t level) +{ + if (!g_log_ready) { + return 1; + } + + return level >= g_log_config.level; +} + +const hybbx_log_config_t *hybbx_log_config_get(void) +{ + return g_log_ready ? &g_log_config : NULL; +} + +int hybbx_log_enabled(void) +{ + return g_log_ready && g_log_config.enabled && g_log_file != NULL; +} + +static void log_close_file(void) +{ + if (g_log_file != NULL) { + fclose(g_log_file); + g_log_file = NULL; + } + g_log_open_week_yyyymmdd = 0; + g_log_open_name[0] = '\0'; +} + +/* Monday of the ISO week containing @p tm (local wall calendar). */ +static int log_week_start_tm(const struct tm *tm, struct tm *out) +{ + time_t t; + int from_monday; + + if (tm == NULL || out == NULL) { + return -1; + } + + *out = *tm; + /* tm_wday: 0=Sun … 6=Sat → days since Monday */ + from_monday = (out->tm_wday + 6) % 7; + out->tm_mday -= from_monday; + out->tm_hour = 12; /* avoid DST midnight edge when normalizing */ + out->tm_min = 0; + out->tm_sec = 0; + out->tm_isdst = -1; + t = mktime(out); + if (t == (time_t)-1) { + return -1; + } + if (localtime_r(&t, out) == NULL) { + return -1; + } + return 0; +} + +static int log_week_yyyymmdd(const struct tm *tm) +{ + struct tm week; + + if (log_week_start_tm(tm, &week) != 0) { + return 0; + } + return (week.tm_year + 1900) * 10000 + + (week.tm_mon + 1) * 100 + + week.tm_mday; +} + +static void log_week_basename(int week_yyyymmdd, char *name, size_t name_len) +{ + snprintf(name, name_len, "%08d-week-hybbx.log", week_yyyymmdd); +} + +static int log_build_path_for_week(char *out, size_t out_len, int week_yyyymmdd) +{ + char name[64]; + + log_week_basename(week_yyyymmdd, name, sizeof(name)); + return hybbx_path_join(out, out_len, g_log_config.dir, name) == HYBBX_OK + ? 0 + : -1; +} + +static int log_is_eight_digits(const char *s) +{ + size_t i; + + for (i = 0; i < 8; i++) { + if (s[i] < '0' || s[i] > '9') { + return 0; + } + } + return 1; +} + +/* + * Packable: current-scheme week files, or legacy daily YYYYMMDD-hybbx.log. + * Never security.log, arc/, or the active week file. + */ +static int log_name_is_packable(const char *name) +{ + size_t len; + + if (name == NULL || name[0] == '\0' || name[0] == '.') { + return 0; + } + if (strcmp(name, g_log_open_name) == 0) { + return 0; + } + + len = strlen(name); + if (len == 22 && log_is_eight_digits(name) && + strcmp(name + 8, "-week-hybbx.log") == 0) { + return 1; + } + /* Legacy daily: 20260720-hybbx.log (8 digits + 10). */ + if (len == 18 && log_is_eight_digits(name) && + strcmp(name + 8, "-hybbx.log") == 0) { + return 1; + } + return 0; +} + +static int log_cmp_names(const void *a, const void *b) +{ + return strcmp(*(const char *const *)a, *(const char *const *)b); +} + +static int log_run_tar_bz2(const char *arc_path, char **basenames, int count) +{ + pid_t pid; + int status; + int i; + char *argv[HYBBX_LOG_PACKABLE_MAX + 8]; + int argc = 0; + + if (arc_path == NULL || basenames == NULL || count <= 0 || + count > HYBBX_LOG_PACKABLE_MAX) { + return -1; + } + + argv[argc++] = "tar"; + argv[argc++] = "-C"; + argv[argc++] = (char *)g_log_config.dir; + argv[argc++] = "-cjf"; + argv[argc++] = (char *)arc_path; + for (i = 0; i < count; i++) { + argv[argc++] = basenames[i]; + } + argv[argc] = NULL; + + pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + execvp("tar", argv); + _exit(127); + } + + if (waitpid(pid, &status, 0) < 0) { + return -1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + return -1; + } + return 0; +} + +static int log_collect_packable(char namebuf[][64], char **names, int max) +{ + DIR *dir; + struct dirent *ent; + int count = 0; + + dir = opendir(g_log_config.dir); + if (dir == NULL) { + return 0; + } + + while ((ent = readdir(dir)) != NULL && count < max) { + if (!log_name_is_packable(ent->d_name)) { + continue; + } + if (strlen(ent->d_name) >= 64) { + continue; + } + hybbx_strlcpy(namebuf[count], ent->d_name, 64); + names[count] = namebuf[count]; + count++; + } + closedir(dir); + return count; +} + +/* Pack up to HYBBX_LOG_PACK_BATCH oldest packable files. Returns packed count. */ +static int log_archive_one_batch(void) +{ + char *names[HYBBX_LOG_PACKABLE_MAX]; + char namebuf[HYBBX_LOG_PACKABLE_MAX][64]; + int count; + int pack_n; + int i; + char arc_dir[HYBBX_PATH_MAX]; + char arc_path[HYBBX_PATH_MAX]; + char arc_name[80]; + struct stat st; + + count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX); + if (count <= 0) { + return 0; + } + + qsort(names, (size_t)count, sizeof(names[0]), log_cmp_names); + pack_n = count < HYBBX_LOG_PACK_BATCH ? count : HYBBX_LOG_PACK_BATCH; + + if (hybbx_path_join(arc_dir, sizeof(arc_dir), g_log_config.dir, + HYBBX_LOG_ARC_SUBDIR) != HYBBX_OK) { + fprintf(stderr, "[log] archive path too long\n"); + return -1; + } + if (mkdir_p(arc_dir) != 0) { + fprintf(stderr, "[log] cannot create %s\n", arc_dir); + return -1; + } + + snprintf(arc_name, sizeof(arc_name), "%.8s-week-hybbx.tar.bz2", names[0]); + if (hybbx_path_join(arc_path, sizeof(arc_path), arc_dir, arc_name) != + HYBBX_OK) { + fprintf(stderr, "[log] archive file path too long\n"); + return -1; + } + if (stat(arc_path, &st) == 0) { + snprintf(arc_name, sizeof(arc_name), + "%.8s-week-hybbx-%ld.tar.bz2", names[0], (long)time(NULL)); + if (hybbx_path_join(arc_path, sizeof(arc_path), arc_dir, arc_name) != + HYBBX_OK) { + return -1; + } + } + + if (log_run_tar_bz2(arc_path, names, pack_n) != 0) { + fprintf(stderr, "[log] tar.bz2 failed for %s — sources kept\n", + arc_path); + return -1; + } + + for (i = 0; i < pack_n; i++) { + char full[HYBBX_PATH_MAX]; + if (hybbx_path_join(full, sizeof(full), g_log_config.dir, names[i]) != + HYBBX_OK) { + continue; + } + if (unlink(full) != 0) { + fprintf(stderr, "[log] cannot remove packed %s\n", full); + } + } + + fprintf(stderr, "[log] archived %d file(s) → %s\n", pack_n, arc_path); + return pack_n; +} + +/* + * @p on_week_switch: day-8 path — pack every closed/legacy file (batches of 7). + * Otherwise (startup): pack only when ≥7 packable (legacy daily sweep). + */ +static void log_archive_packable(int on_week_switch) +{ + char *names[HYBBX_LOG_PACKABLE_MAX]; + char namebuf[HYBBX_LOG_PACKABLE_MAX][64]; + int count; + int guard; + + if (g_log_config.dir[0] == '\0') { + return; + } + + count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX); + if (count <= 0) { + return; + } + if (!on_week_switch && count < HYBBX_LOG_PACK_BATCH) { + return; + } + + for (guard = 0; guard < HYBBX_LOG_PACKABLE_MAX; guard++) { + int packed = log_archive_one_batch(); + if (packed <= 0) { + break; + } + count = log_collect_packable(namebuf, names, HYBBX_LOG_PACKABLE_MAX); + if (count <= 0) { + break; + } + if (!on_week_switch && count < HYBBX_LOG_PACK_BATCH) { + break; + } + } +} + +hybbx_result_t hybbx_log_current_path(char *out, size_t out_len) +{ + time_t now; + struct tm tm_buf; + struct tm *tm; + int week; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + out[0] = '\0'; + if (!g_log_ready || !g_log_config.enabled || g_log_config.dir[0] == '\0') { + return HYBBX_ERR_NOT_FOUND; + } + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + return HYBBX_ERR_IO; + } + + week = log_week_yyyymmdd(tm); + if (week == 0 || log_build_path_for_week(out, out_len, week) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static int log_open_for_time(const struct tm *tm) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + int week; + int rotated = 0; + + if (tm == NULL) { + return -1; + } + + week = log_week_yyyymmdd(tm); + if (week == 0) { + return -1; + } + + if (g_log_file != NULL && g_log_open_week_yyyymmdd == week) { + return 0; + } + + if (g_log_file != NULL) { + rotated = 1; + } + + log_close_file(); + + if (mkdir_p(g_log_config.dir) != 0) { + fprintf(stderr, "[log] cannot create directory %s\n", g_log_config.dir); + return -1; + } + + if (log_build_path_for_week(path, sizeof(path), week) != 0) { + fprintf(stderr, "[log] path too long for log file\n"); + return -1; + } + + fp = fopen(path, "a"); + if (fp == NULL) { + fprintf(stderr, "[log] cannot open %s\n", path); + return -1; + } + + g_log_file = fp; + g_log_open_week_yyyymmdd = week; + log_week_basename(week, g_log_open_name, sizeof(g_log_open_name)); + + /* Day-8 switch packs closed week (+ legacy). Startup sweeps if ≥7. */ + log_archive_packable(rotated); + + return 0; +} + +static int log_ensure_open(void) +{ + time_t now; + struct tm tm_buf; + struct tm *tm; + + if (!g_log_config.enabled || g_log_config.dir[0] == '\0') { + return -1; + } + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + return -1; + } + + return log_open_for_time(tm); +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_log_config_apply(const struct hybbx_config *config) +{ + const char *dir_raw; + const char *level_raw; + hybbx_log_level_t parsed; + + hybbx_log_shutdown(); + hybbx_log_config_defaults(&g_log_config); + + if (config != NULL) { + g_log_config.enabled = + hybbx_config_get_bool(config, "log", "enabled", 1); + dir_raw = hybbx_config_get(config, "log", "dir", NULL); + level_raw = hybbx_config_get(config, "log", "level", "warn"); + + parsed = hybbx_log_parse_level(level_raw); + if (level_raw != NULL && level_raw[0] != '\0' && + strcmp(level_raw, hybbx_log_level_name(parsed)) != 0 && + strcasecmp(level_raw, hybbx_log_level_name(parsed)) != 0) { + fprintf(stderr, + "[log] unknown level '%s' — using warn (debug|stats|info|warn)\n", + level_raw); + } + g_log_config.level = parsed; + + if (dir_raw != NULL && dir_raw[0] != '\0') { + if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir), + dir_raw) != HYBBX_OK) { + fprintf(stderr, "[log] invalid dir path\n"); + g_log_config.enabled = 0; + } + } else if (g_log_config.enabled) { + if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + fprintf(stderr, "[log] cannot resolve default log directory\n"); + g_log_config.enabled = 0; + } + } + } else { + if (hybbx_path_resolve(g_log_config.dir, sizeof(g_log_config.dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + g_log_config.enabled = 0; + } + } + + g_log_ready = 1; + + if (g_log_config.enabled) { + if (log_ensure_open() != 0) { + g_log_config.enabled = 0; + } + } + + printf("[log] enabled=%s dir=%s level=%s week=%s\n", + hybbx_bool_to_string(g_log_config.enabled), + g_log_config.dir[0] != '\0' ? g_log_config.dir : "-", + hybbx_log_level_name(g_log_config.level), + g_log_open_name[0] != '\0' ? g_log_open_name : "-"); +} +#else +void hybbx_log_config_apply(const struct hybbx_config *config) +{ + (void)config; + hybbx_log_config_defaults(&g_log_config); + g_log_ready = 1; +} +#endif + +static void log_write_console(hybbx_log_level_t level, const char *message) +{ + FILE *out = (level == HYBBX_LOG_WARN) ? stderr : stdout; + + fputs(message, out); + fputc('\n', out); + fflush(out); +} + +static void log_write_file(hybbx_log_level_t level, const char *message) +{ + char line[HYBBX_LOG_LINE_MAX + 64]; + time_t now; + struct tm tm_buf; + struct tm *tm; + + if (!g_log_config.enabled) { + return; + } + + pthread_mutex_lock(&g_log_lock); + + if (log_ensure_open() != 0) { + pthread_mutex_unlock(&g_log_lock); + return; + } + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + pthread_mutex_unlock(&g_log_lock); + return; + } + + if (hybbx_time_format_stamp(line, sizeof(line), tm, NULL) != HYBBX_OK) { + pthread_mutex_unlock(&g_log_lock); + return; + } + + { + size_t stamp_len = strlen(line); + snprintf(line + stamp_len, sizeof(line) - stamp_len, + " [%s] %s\n", hybbx_log_level_name(level), message); + } + + fputs(line, g_log_file); + fflush(g_log_file); + + pthread_mutex_unlock(&g_log_lock); +} + +void hybbx_log_write(hybbx_log_level_t level, const char *fmt, ...) +{ + char message[HYBBX_LOG_LINE_MAX]; + va_list ap; + + if (fmt == NULL) { + return; + } + + if (g_log_ready && !hybbx_log_level_visible(level)) { + return; + } + + va_start(ap, fmt); + vsnprintf(message, sizeof(message), fmt, ap); + va_end(ap); + + log_write_console(level, message); + if (g_log_ready) { + log_write_file(level, message); + } +} + +void hybbx_log_shutdown(void) +{ + pthread_mutex_lock(&g_log_lock); + log_close_file(); + g_log_ready = 0; + hybbx_log_config_defaults(&g_log_config); + pthread_mutex_unlock(&g_log_lock); +} diff --git a/src/core/mail.c b/src/core/mail.c new file mode 100644 index 0000000..b3aff97 --- /dev/null +++ b/src/core/mail.c @@ -0,0 +1,1532 @@ +#include "hybbx/mail.h" +#include "hybbx/messages.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "mail_sql.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_MAIL_DIR_NAME "mail" +#define HYBBX_MAIL_INBOX_NAME "inbox" +#define HYBBX_MAIL_RECYCLE_NAME "recycle" +#define HYBBX_MAIL_NEXT_FILE "mail.next" + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t read_counter(const char *path, uint64_t *value) +{ + FILE *fp; + unsigned long long n = 0; + + fp = fopen(path, "r"); + if (fp == NULL) { + *value = 0; + return HYBBX_OK; + } + + if (fscanf(fp, "%llu", &n) != 1) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + *value = (uint64_t)n; + return HYBBX_OK; +} + +static hybbx_result_t write_counter(const char *path, uint64_t value) +{ + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", (unsigned long long)value); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t mail_user_inbox_path(const hybbx_mail_config_t *mail, + const char *username, + char *out, size_t out_len) +{ + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (mail == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), mail->root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_MAIL_INBOX_NAME); +} + +static hybbx_result_t mail_user_recycle_path(const hybbx_mail_config_t *mail, + const char *username, + char *out, size_t out_len) +{ + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (mail == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), mail->root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_MAIL_RECYCLE_NAME); +} + +static int parse_msg_filename(const char *name, uint64_t *out_id) +{ + char *end; + unsigned long long id; + + if (name == NULL || out_id == NULL) { + return 0; + } + + if (strlen(name) < 5 || strcmp(name + strlen(name) - 4, ".msg") != 0) { + return 0; + } + + id = strtoull(name, &end, 10); + if (end == name || strcmp(end, ".msg") != 0) { + return 0; + } + + *out_id = (uint64_t)id; + return 1; +} + +static int mail_entry_compare(const void *a, const void *b) +{ + const hybbx_mail_entry_t *ea = (const hybbx_mail_entry_t *)a; + const hybbx_mail_entry_t *eb = (const hybbx_mail_entry_t *)b; + + if (ea->received_at > eb->received_at) { + return -1; + } + if (ea->received_at < eb->received_at) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + if (ea->id < eb->id) { + return 1; + } + return 0; +} + +static hybbx_result_t load_inbox(const char *inbox_path, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + DIR *dir; + struct dirent *ent; + size_t count = 0; + + if (inbox_path == NULL || entries == NULL || out_count == NULL) { + return HYBBX_ERR_INVALID; + } + + *out_count = 0; + + dir = opendir(inbox_path); + if (dir == NULL) { + if (errno == ENOENT) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + hybbx_mail_entry_t entry; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (count >= max_entries) { + break; + } + + memset(&entry, 0, sizeof(entry)); + entry.id = id; + + if (hybbx_path_join(path, sizeof(path), inbox_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + continue; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *eq; + char *key; + char *value; + + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + break; + } + + eq = strchr(line, '='); + if (eq == NULL) { + continue; + } + + *eq = '\0'; + key = line; + value = eq + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + + { + size_t vlen = strlen(value); + + while (vlen > 0 && (value[vlen - 1] == '\n' || + value[vlen - 1] == '\r')) { + value[--vlen] = '\0'; + } + } + + if (str_ieq(key, "from")) { + hybbx_strlcpy(entry.from, value, sizeof(entry.from)); + } else if (str_ieq(key, "subject")) { + hybbx_strlcpy(entry.subject, value, sizeof(entry.subject)); + } else if (str_ieq(key, "time")) { + entry.received_at = (time_t)strtol(value, NULL, 10); + } else if (str_ieq(key, "read")) { + entry.read = hybbx_bool_is_true(value); + } + } + + fclose(fp); + entries[count++] = entry; + } + + closedir(dir); + + if (count > 1) { + qsort(entries, count, sizeof(entries[0]), mail_entry_compare); + } + + *out_count = count; + return HYBBX_OK; +} + +static int mail_uses_sqlite(hybbx_service_t *service) +{ + hybbx_storage_t *storage; + + if (service == NULL) { + return 0; + } + + storage = hybbx_service_get_storage(service); + return storage != NULL && + hybbx_storage_backend(storage) == HYBBX_STORAGE_SQLITE; +} + +static hybbx_result_t mail_load_inbox(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + hybbx_storage_t *storage; + char inbox[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (mail_uses_sqlite(service)) { + storage = hybbx_service_get_storage(service); + return hybbx_mail_sql_load_inbox(storage, username, entries, + max_entries, out_count); + } + + rc = mail_user_inbox_path(mail, username, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + return load_inbox(inbox, entries, max_entries, out_count); +} + +static hybbx_result_t msg_file_path(const char *inbox_path, uint64_t id, + char *out, size_t out_len) +{ + char name[32]; + + snprintf(name, sizeof(name), "%06llu.msg", + (unsigned long long)id); + return hybbx_path_join(out, out_len, inbox_path, name); +} + +void hybbx_mail_config_defaults(hybbx_mail_config_t *mail) +{ + if (mail == NULL) { + return; + } + + mail->enabled = 1; + mail->max_messages = HYBBX_MAIL_MAX_MESSAGES; + mail->subject_max = HYBBX_MAIL_SUBJECT_MAX; + mail->body_max = HYBBX_MAIL_BODY_MAX; + mail->recycle_days = HYBBX_MAIL_DEFAULT_RECYCLE_DAYS; + mail->root[0] = '\0'; +} + +void hybbx_mail_config_apply(hybbx_mail_config_t *mail, + const hybbx_config_t *config, + const char *storage_path) +{ + if (mail == NULL) { + return; + } + + hybbx_mail_config_defaults(mail); + + if (config != NULL) { + mail->enabled = hybbx_config_get_bool(config, "mail", "enabled", 1); + mail->max_messages = hybbx_config_get_uint( + config, "mail", "max_messages", HYBBX_MAIL_MAX_MESSAGES, 1u, + HYBBX_MAIL_MAX_MESSAGES); + mail->subject_max = hybbx_config_get_uint( + config, "mail", "subject_max", HYBBX_MAIL_SUBJECT_MAX, 8u, + HYBBX_MAIL_SUBJECT_MAX); + mail->body_max = hybbx_config_get_uint( + config, "mail", "body_max", HYBBX_MAIL_BODY_MAX, 64u, + HYBBX_MAIL_BODY_MAX); + mail->recycle_days = hybbx_config_get_uint( + config, "mail", "recycle_days", HYBBX_MAIL_DEFAULT_RECYCLE_DAYS, + 1u, 365u); + } + + mail->root[0] = '\0'; + if (storage_path != NULL && storage_path[0] != '\0') { + const char *subdir = "mail"; + const char *custom; + + if (config != NULL) { + custom = hybbx_config_get(config, "mail", "path", NULL); + if (custom != NULL && custom[0] != '\0') { + subdir = custom; + } + } + + if (hybbx_path_join(mail->root, sizeof(mail->root), storage_path, + subdir) != HYBBX_OK) { + mail->root[0] = '\0'; + } + } + + hybbx_log_info("[mail] enabled=%s max_messages=%u recycle_days=%u root=%s", + hybbx_bool_to_string(mail->enabled), mail->max_messages, + mail->recycle_days, + mail->root[0] != '\0' ? mail->root : "(unset)"); +} + +static hybbx_result_t mail_ensure_root(const hybbx_mail_config_t *mail) +{ + char next_path[HYBBX_PATH_MAX]; + + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail->root[0] == '\0') { + return HYBBX_ERR_IO; + } + + if (mkdir_p(mail->root) != 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(next_path, sizeof(next_path), mail->root, + HYBBX_MAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + { + FILE *fp = fopen(next_path, "r"); + + if (fp == NULL) { + return write_counter(next_path, 0); + } + fclose(fp); + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_next_id(const hybbx_mail_config_t *mail, + uint64_t *out_id) +{ + char next_path[HYBBX_PATH_MAX]; + uint64_t id; + hybbx_result_t rc; + + if (hybbx_path_join(next_path, sizeof(next_path), mail->root, + HYBBX_MAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = read_counter(next_path, &id); + if (rc != HYBBX_OK) { + return rc; + } + + id++; + rc = write_counter(next_path, id); + if (rc != HYBBX_OK) { + return rc; + } + + *out_id = id; + return HYBBX_OK; +} + +static hybbx_result_t mail_trim_inbox(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username, + unsigned max_messages) +{ + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + size_t i; + hybbx_result_t rc; + char inbox[HYBBX_PATH_MAX]; + + if (mail_uses_sqlite(service)) { + return HYBBX_OK; + } + + rc = mail_user_inbox_path(mail, username, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_inbox(inbox, entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (count <= max_messages) { + return HYBBX_OK; + } + + for (i = max_messages; i < count; i++) { + char path[HYBBX_PATH_MAX]; + + if (msg_file_path(inbox, entries[i].id, path, sizeof(path)) == + HYBBX_OK) { + remove(path); + } + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_ensure_storage(hybbx_service_t *service, + const hybbx_mail_config_t *mail) +{ + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail_uses_sqlite(service)) { + return HYBBX_OK; + } + + return mail_ensure_root(mail); +} + +void hybbx_mail_list_inbox(hybbx_service_t *service, hybbx_session_t *session) +{ + hybbx_mail_list_inbox_range(service, session, 1, 0); +} + +static const char *mail_display_name(hybbx_service_t *service, + const char *name, + char *buf, size_t buf_len) +{ + hybbx_storage_t *storage; + hybbx_user_record_t user; + + if (name == NULL || name[0] == '\0') { + return ""; + } + + storage = hybbx_service_get_storage(service); + if (storage != NULL && + hybbx_storage_resolve_user(storage, name, &user) == HYBBX_OK) { + return hybbx_user_display_name(&user); + } + + hybbx_strlcpy(buf, name, buf_len); + return buf; +} + +static void mail_list_print(hybbx_service_t *service, + hybbx_session_t *session, + const hybbx_mail_entry_t *entries, + size_t count, + unsigned from, + unsigned to) +{ + size_t i; + size_t start; + size_t end; + char line[HYBBX_MAIL_SUBJECT_MAX + 64]; + char header[48]; + char from_name[HYBBX_USER_NAME_MAX]; + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (count == 0) { + hybbx_session_write_line(session, "Inbox empty."); + hybbx_session_write_line(session, + "Use /mail send to compose."); + return; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "No messages in that range."); + return; + } + + start = (size_t)(from - 1); + end = (size_t)to; + + if (from == 1 && to == (unsigned)count) { + hybbx_session_write_line(session, "Inbox (newest first):"); + } else { + snprintf(header, sizeof(header), "Inbox %u-%u (newest first):", + from, to); + hybbx_session_write_line(session, header); + } + + for (i = start; i < end; i++) { + snprintf(line, sizeof(line), " %zu %s%s %s", + i + 1, + entries[i].read ? " " : "* ", + mail_display_name(service, entries[i].from, from_name, + sizeof(from_name)), + entries[i].subject[0] != '\0' ? entries[i].subject + : "(no subject)"); + hybbx_session_write_line(session, line); + } + + hybbx_session_write_line(session, + " /mail read delete list recycle"); +} + +static unsigned mail_recycle_purge_expired(const hybbx_mail_config_t *mail, + const char *recycle_path) +{ + DIR *dir; + struct dirent *ent; + time_t now = time(NULL); + time_t max_age; + unsigned removed = 0; + + if (mail == NULL || recycle_path == NULL || recycle_path[0] == '\0') { + return 0; + } + + max_age = (time_t)mail->recycle_days * 86400; + + dir = opendir(recycle_path); + if (dir == NULL) { + if (errno == ENOENT) { + return 0; + } + return 0; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + struct stat st; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (hybbx_path_join(path, sizeof(path), recycle_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + if (stat(path, &st) != 0) { + continue; + } + + if (now - st.st_mtime >= max_age) { + if (remove(path) == 0) { + removed++; + } + } + } + + closedir(dir); + return removed; +} + +static hybbx_result_t mail_move_id_to_recycle(const char *inbox_path, + const char *recycle_path, + uint64_t id) +{ + char src[HYBBX_PATH_MAX]; + char dst[HYBBX_PATH_MAX]; + FILE *in_fp; + FILE *out_fp; + char line[HYBBX_LINE_MAX]; + char tmp[HYBBX_PATH_MAX + 8]; + int has_deleted = 0; + + if (msg_file_path(inbox_path, id, src, sizeof(src)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (mkdir_p(recycle_path) != 0) { + return HYBBX_ERR_IO; + } + + if (msg_file_path(recycle_path, id, dst, sizeof(dst)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (strlen(dst) + 4 >= sizeof(tmp)) { + return HYBBX_ERR_IO; + } + snprintf(tmp, sizeof(tmp), "%s.tmp", dst); + + in_fp = fopen(src, "r"); + out_fp = fopen(tmp, "w"); + if (in_fp == NULL || out_fp == NULL) { + if (in_fp != NULL) { + fclose(in_fp); + } + if (out_fp != NULL) { + fclose(out_fp); + } + remove(tmp); + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), in_fp) != NULL) { + if (strncmp(line, "deleted=", 8) == 0) { + continue; + } + if (!has_deleted && line[0] == '-' && line[1] == '-' && line[2] == '-') { + fprintf(out_fp, "deleted=%ld\n", (long)time(NULL)); + has_deleted = 1; + } + fputs(line, out_fp); + } + + if (!has_deleted) { + fprintf(out_fp, "deleted=%ld\n", (long)time(NULL)); + } + + fclose(in_fp); + fclose(out_fp); + + if (remove(src) != 0) { + remove(tmp); + return HYBBX_ERR_IO; + } + + if (rename(tmp, dst) != 0) { + remove(tmp); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t mail_purge_user_recycle(hybbx_service_t *service, + const hybbx_mail_config_t *mail, + const char *username) +{ + char recycle[HYBBX_PATH_MAX]; + + if (mail_uses_sqlite(service)) { + (void)hybbx_mail_sql_purge_recycle(hybbx_service_get_storage(service), + mail, username); + return HYBBX_OK; + } + + if (mail_user_recycle_path(mail, username, recycle, sizeof(recycle)) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + (void)mail_recycle_purge_expired(mail, recycle); + return HYBBX_OK; +} + +void hybbx_mail_list_inbox_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + size_t count; + hybbx_result_t rc; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Mail storage unavailable."); + return; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Mail path error."); + return; + } + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Cannot read inbox."); + return; + } + + mail_list_print(service, session, entries, count, from, to); +} + +void hybbx_mail_announce_since_last_login(hybbx_service_t *service, + hybbx_session_t *session, + time_t since_login) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + size_t count; + size_t i; + size_t new_count = 0; + hybbx_result_t rc; + + if (service == NULL || session == NULL) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + return; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return; + } + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK || count == 0) { + return; + } + + for (i = 0; i < count; i++) { + if (since_login == 0 || entries[i].received_at > since_login) { + new_count++; + } + } + + if (new_count == 0) { + return; + } + + if (new_count == 1) { + (void)hybbx_msg_send_system(session, + "You have 1 new message since your last login. (/mail)"); + } else { + char body[96]; + snprintf(body, sizeof(body), + "You have %zu new messages since your last login. (/mail)", + new_count); + (void)hybbx_msg_send_system(session, body); + } +} + +int hybbx_mail_parse_list_range(const char *spec, + unsigned *from, unsigned *to) +{ + const char *dash; + char *end; + unsigned long start; + unsigned long end_num; + + if (from == NULL || to == NULL) { + return 0; + } + + if (spec == NULL || spec[0] == '\0') { + *from = 1; + *to = 0; + return 1; + } + + dash = strchr(spec, '-'); + if (dash == NULL) { + start = strtoul(spec, &end, 10); + if (end == spec || *end != '\0' || start == 0) { + return 0; + } + *from = (unsigned)start; + *to = (unsigned)start; + return 1; + } + + start = strtoul(spec, &end, 10); + if (end != dash || start == 0) { + return 0; + } + + end_num = strtoul(dash + 1, &end, 10); + if (end == dash + 1 || *end != '\0' || end_num == 0) { + return 0; + } + + if (start > end_num) { + return 0; + } + + *from = (unsigned)start; + *to = (unsigned)end_num; + return 1; +} + +static hybbx_result_t mail_resolve_index(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index, + uint64_t *out_id, + char *inbox_path, size_t inbox_len) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + hybbx_result_t rc; + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = mail_ensure_storage(service, mail); + if (rc != HYBBX_OK) { + return rc; + } + + if (!mail_uses_sqlite(service)) { + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox_path, inbox_len); + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (list_index == 0 || list_index > count) { + return HYBBX_ERR_NOT_FOUND; + } + + *out_id = entries[list_index - 1].id; + return HYBBX_OK; +} + +static void mail_print_header_line(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char out_line[HYBBX_LINE_MAX]; + char name_buf[HYBBX_USER_NAME_MAX]; + char fallback[HYBBX_USER_NAME_MAX]; + const char *value; + const char *display; + size_t vlen; + + if (line == NULL || session == NULL) { + return; + } + + if (strncmp(line, "from=", 5) == 0) { + value = line + 5; + vlen = strlen(value); + while (vlen > 0 && (value[vlen - 1] == '\n' || value[vlen - 1] == '\r')) { + vlen--; + } + memcpy(name_buf, value, vlen); + name_buf[vlen] = '\0'; + display = mail_display_name(service, name_buf, fallback, sizeof(fallback)); + snprintf(out_line, sizeof(out_line), "from=%s", display); + hybbx_session_write_line(session, out_line); + return; + } + + if (strncmp(line, "to=", 3) == 0) { + value = line + 3; + vlen = strlen(value); + while (vlen > 0 && (value[vlen - 1] == '\n' || value[vlen - 1] == '\r')) { + vlen--; + } + memcpy(name_buf, value, vlen); + name_buf[vlen] = '\0'; + display = mail_display_name(service, name_buf, fallback, sizeof(fallback)); + snprintf(out_line, sizeof(out_line), "to=%s", display); + hybbx_session_write_line(session, out_line); + return; + } + + hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_mail_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + char inbox[HYBBX_PATH_MAX]; + char path[HYBBX_PATH_MAX]; + uint64_t id; + FILE *fp; + char line[HYBBX_LINE_MAX]; + int in_body = 0; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + if (mail_uses_sqlite(service)) { + (void)hybbx_mail_sql_purge_recycle(hybbx_service_get_storage(service), + hybbx_service_get_mail(service), + hybbx_session_username(session)); + return hybbx_mail_sql_read(service, session, list_index); + } + + (void)mail_purge_user_recycle(service, hybbx_service_get_mail(service), + hybbx_session_username(session)); + + rc = mail_resolve_index(service, session, list_index, &id, + inbox, sizeof(inbox)); + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + if (msg_file_path(inbox, id, path, sizeof(path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + hybbx_session_write_line(session, "Cannot open message."); + return HYBBX_ERR_IO; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + if (!in_body) { + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + in_body = 1; + continue; + } + if (strncmp(line, "read=", 5) == 0) { + continue; + } + if (strncmp(line, "from=", 5) == 0 || + strncmp(line, "to=", 3) == 0) { + mail_print_header_line(service, session, line); + continue; + } + } + hybbx_session_write_line(session, line); + } + + fclose(fp); + + { + char tmp_path[HYBBX_PATH_MAX + 8]; + FILE *in_fp; + FILE *out_fp; + char buf[HYBBX_LINE_MAX]; + + if (strlen(path) + 4 >= sizeof(tmp_path)) { + return HYBBX_OK; + } + snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); + in_fp = fopen(path, "r"); + out_fp = fopen(tmp_path, "w"); + if (in_fp != NULL && out_fp != NULL) { + while (fgets(buf, sizeof(buf), in_fp) != NULL) { + if (strncmp(buf, "read=", 5) == 0) { + fputs("read=yes\n", out_fp); + } else { + fputs(buf, out_fp); + } + } + fclose(in_fp); + fclose(out_fp); + remove(path); + rename(tmp_path, path); + } else { + if (in_fp != NULL) { + fclose(in_fp); + } + if (out_fp != NULL) { + fclose(out_fp); + } + remove(tmp_path); + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char inbox[HYBBX_PATH_MAX]; + char recycle[HYBBX_PATH_MAX]; + size_t count; + size_t i; + unsigned moved = 0; + hybbx_result_t rc; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return HYBBX_ERR_UNSUPPORTED; + } + + if (to == 0) { + to = from; + } + if (from == 0 || from > to) { + hybbx_session_write_line(session, "Usage: /mail delete "); + return HYBBX_OK; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_delete_range(service, session, from, to); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_inbox_path(mail, hybbx_session_username(session), + inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_recycle_path(mail, hybbx_session_username(session), + recycle, sizeof(recycle)); + if (rc != HYBBX_OK) { + return rc; + } + + (void)mail_purge_user_recycle(service, mail, hybbx_session_username(session)); + + rc = mail_load_inbox(service, mail, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (from > count || to > count) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + rc = mail_move_id_to_recycle(inbox, recycle, entries[i].id); + if (rc != HYBBX_OK) { + hybbx_session_write_line(session, "Delete failed."); + return rc; + } + moved++; + } + + if (moved == 1) { + hybbx_session_write_line(session, + "Message moved to recycle (auto-purge after configured days)."); + } else { + char buf[64]; + + snprintf(buf, sizeof(buf), + "%u messages moved to recycle (auto-purge after %u days).", + moved, mail->recycle_days); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_delete(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + return hybbx_mail_delete_range(service, session, list_index, list_index); +} + +hybbx_result_t hybbx_mail_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_mail_config_t *mail; + char recycle[HYBBX_PATH_MAX]; + DIR *dir; + struct dirent *ent; + unsigned removed = 0; + hybbx_result_t rc; + char buf[64]; + + if (hybbx_session_is_guest(session)) { + hybbx_session_write_line(session, "Guests cannot use mail."); + return HYBBX_ERR_DENIED; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + hybbx_session_write_line(session, "Mail is disabled."); + return HYBBX_ERR_UNSUPPORTED; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_recycle_empty(service, session); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_recycle_path(mail, hybbx_session_username(session), + recycle, sizeof(recycle)); + if (rc != HYBBX_OK) { + return rc; + } + + (void)mail_recycle_purge_expired(mail, recycle); + + dir = opendir(recycle); + if (dir == NULL) { + if (errno == ENOENT) { + hybbx_session_write_line(session, "Recycle bin empty."); + return HYBBX_OK; + } + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (hybbx_path_join(path, sizeof(path), recycle, ent->d_name) != + HYBBX_OK) { + continue; + } + + if (remove(path) == 0) { + removed++; + } + } + + closedir(dir); + + if (removed == 0) { + hybbx_session_write_line(session, "Recycle bin empty."); + } else { + snprintf(buf, sizeof(buf), "Recycle bin emptied (%u message(s)).", + removed); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +#define HYBBX_MAIL_SYSTEM_FROM "system" + +static void mail_format_utc_time(time_t when, char *buf, size_t len) +{ + if (buf == NULL || len == 0) { + return; + } + + buf[0] = '\0'; + if (when <= 0) { + return; + } + + { + const struct tm *tm = gmtime(&when); + + if (tm != NULL) { + (void)strftime(buf, len, "%Y-%m-%d %H:%M:%S UTC", tm); + } + } +} + +typedef struct mail_notify_staff_ctx { + hybbx_service_t *service; + char subject[HYBBX_MAIL_SUBJECT_MAX]; + char body[HYBBX_MAIL_BODY_MAX]; +} mail_notify_staff_ctx_t; + +static hybbx_result_t mail_notify_staff_cb(const hybbx_user_record_t *user, + void *ctx) +{ + mail_notify_staff_ctx_t *nctx = (mail_notify_staff_ctx_t *)ctx; + + if (user == NULL || nctx == NULL) { + return HYBBX_OK; + } + + if (!user->active || !hybbx_user_level_is_sysop_or_admin(user->level)) { + return HYBBX_OK; + } + + (void)hybbx_mail_deliver(nctx->service, HYBBX_MAIL_SYSTEM_FROM, + user->username, nctx->subject, nctx->body); + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_notify_staff_registration(hybbx_service_t *service, + const hybbx_user_registration_t *reg, + const hybbx_user_record_t *user) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + mail_notify_staff_ctx_t ctx; + char created[48]; + int n; + + if (service == NULL || reg == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_OK; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + n = snprintf(ctx.subject, sizeof(ctx.subject), + "Registration pending: %s", reg->nickname); + if (n < 0 || (size_t)n >= sizeof(ctx.subject)) { + return HYBBX_ERR_INVALID; + } + + mail_format_utc_time(user->created_at, created, sizeof(created)); + if (created[0] == '\0') { + hybbx_strlcpy(created, "(unknown)", sizeof(created)); + } + + n = snprintf(ctx.body, sizeof(ctx.body), + "Guest self-registration pending approval.\n" + "/register data submitted:\n\n" + " Nickname: %s\n" + " Login: %s\n" + " Full name: %s\n" + " Country: %s\n" + " Location: %s\n" + " Email: %s\n\n" + "Account record:\n\n" + " User ID: %llu\n" + " Level: %s\n" + " Active: %s\n" + " Created: %s\n\n" + "Review all fields. If correct, activate with:\n" + " /activate %s\n", + reg->nickname, + reg->username, + reg->full_name, + reg->country, + reg->location, + reg->email, + (unsigned long long)user->id, + hybbx_user_level_name(user->level), + hybbx_bool_to_string(user->active), + created, + reg->username); + if (n < 0 || (size_t)n >= sizeof(ctx.body)) { + return HYBBX_ERR_INVALID; + } + + ctx.service = service; + + (void)hybbx_storage_foreach_user(storage, mail_notify_staff_cb, &ctx); + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_deliver(hybbx_service_t *service, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + hybbx_user_record_t recipient; + char inbox[HYBBX_PATH_MAX]; + char path[HYBBX_PATH_MAX]; + uint64_t id; + FILE *fp; + hybbx_result_t rc; + char to_norm[HYBBX_USER_NAME_MAX]; + size_t body_len; + + if (service == NULL || from_user == NULL || to_user == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + body_len = strlen(body); + if (strlen(subject) > mail->subject_max || body_len > mail->body_max) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(to_norm, to_user, sizeof(to_norm)); + hybbx_username_normalize(to_norm); + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_resolve_user(storage, to_user, &recipient); + if (rc == HYBBX_ERR_NOT_FOUND) { + return HYBBX_ERR_NOT_FOUND; + } + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_strlcpy(to_norm, recipient.username, sizeof(to_norm)); + + if (hybbx_user_level_is_guest(recipient.level) || !recipient.active) { + return HYBBX_ERR_DENIED; + } + + if (mail_uses_sqlite(service)) { + return hybbx_mail_sql_deliver(storage, mail, from_user, to_norm, + subject, body); + } + + rc = mail_ensure_root(mail); + if (rc != HYBBX_OK) { + return rc; + } + + rc = mail_user_inbox_path(mail, to_norm, inbox, sizeof(inbox)); + if (rc != HYBBX_OK) { + return rc; + } + + if (mkdir_p(inbox) != 0) { + return HYBBX_ERR_IO; + } + + rc = mail_next_id(mail, &id); + if (rc != HYBBX_OK) { + return rc; + } + + if (msg_file_path(inbox, id, path, sizeof(path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "id=%llu\n", (unsigned long long)id); + fprintf(fp, "from=%s\n", from_user); + fprintf(fp, "to=%s\n", to_norm); + fprintf(fp, "subject=%s\n", subject); + fprintf(fp, "time=%ld\n", (long)time(NULL)); + fprintf(fp, "read=no\n"); + fprintf(fp, "---\n"); + fputs(body, fp); + if (body_len == 0 || body[body_len - 1] != '\n') { + fputc('\n', fp); + } + fclose(fp); + + (void)mail_trim_inbox(service, mail, to_norm, mail->max_messages); + return HYBBX_OK; +} diff --git a/src/core/mail_sql.c b/src/core/mail_sql.c new file mode 100644 index 0000000..95b93f9 --- /dev/null +++ b/src/core/mail_sql.c @@ -0,0 +1,655 @@ +#include "mail_sql.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" +#include "storage_private.h" + +#include +#include +#include +#include + +#ifdef HYBBX_HAVE_SQLITE +#include + +#define MAIL_FOLDER_INBOX 0 +#define MAIL_FOLDER_RECYCLE 1 + +static int mail_entry_compare(const void *a, const void *b) +{ + const hybbx_mail_entry_t *ea = (const hybbx_mail_entry_t *)a; + const hybbx_mail_entry_t *eb = (const hybbx_mail_entry_t *)b; + + if (ea->received_at > eb->received_at) { + return -1; + } + if (ea->received_at < eb->received_at) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + if (ea->id < eb->id) { + return 1; + } + return 0; +} + +static hybbx_result_t mail_sql_meta_bump(sqlite3 *db, const char *key, + uint64_t *value) +{ + sqlite3_stmt *stmt; + int rc; + + *value = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT value FROM meta WHERE key=?1;", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + if (sqlite3_step(stmt) == SQLITE_ROW) { + *value = (uint64_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + } + + (*value)++; + + rc = sqlite3_prepare_v2(db, + "INSERT INTO meta(key,value) VALUES(?1,?2) " + "ON CONFLICT(key) DO UPDATE SET value=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)*value); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +static void mail_sql_owner_norm(char *owner, size_t len, const char *username) +{ + hybbx_strlcpy(owner, username, len); + hybbx_username_normalize(owner); +} + +hybbx_result_t hybbx_mail_sql_load_inbox(hybbx_storage_t *storage, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + int rc; + size_t count = 0; + + if (storage == NULL || username == NULL || entries == NULL || + out_count == NULL) { + return HYBBX_ERR_INVALID; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), username); + *out_count = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT id,from_user,subject,received_at,read_flag " + "FROM messages WHERE owner=?1 AND folder=?2 " + "ORDER BY received_at DESC, id DESC;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_INBOX); + + while (sqlite3_step(stmt) == SQLITE_ROW && count < max_entries) { + hybbx_mail_entry_t *entry = &entries[count]; + + memset(entry, 0, sizeof(*entry)); + entry->id = (uint64_t)sqlite3_column_int64(stmt, 0); + hybbx_strlcpy(entry->from, (const char *)sqlite3_column_text(stmt, 1), + sizeof(entry->from)); + hybbx_strlcpy(entry->subject, + (const char *)sqlite3_column_text(stmt, 2), + sizeof(entry->subject)); + entry->received_at = (time_t)sqlite3_column_int64(stmt, 3); + entry->read = sqlite3_column_int(stmt, 4); + count++; + } + + sqlite3_finalize(stmt); + + if (count > 1) { + qsort(entries, count, sizeof(entries[0]), mail_entry_compare); + } + + *out_count = count; + return HYBBX_OK; +} + +static hybbx_result_t mail_sql_trim_inbox(sqlite3 *db, const char *owner, + unsigned max_messages) +{ + sqlite3_stmt *stmt; + int rc; + size_t count = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT COUNT(*) FROM messages " + "WHERE owner=?1 AND folder=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_INBOX); + if (sqlite3_step(stmt) == SQLITE_ROW) { + count = (size_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + + if (count <= max_messages) { + return HYBBX_OK; + } + + rc = sqlite3_prepare_v2(db, + "DELETE FROM messages WHERE id IN (" + "SELECT id FROM messages WHERE owner=?1 " + "AND folder=?2 ORDER BY received_at ASC, id ASC " + "LIMIT ?3);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_INBOX); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)(count - max_messages)); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_mail_sql_deliver(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + uint64_t id; + int rc; + hybbx_result_t hres; + time_t now = time(NULL); + + if (storage == NULL || mail == NULL || from_user == NULL || + to_user == NULL) { + return HYBBX_ERR_INVALID; + } + + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), to_user); + + hres = mail_sql_meta_bump(db, "mail_next", &id); + if (hres != HYBBX_OK) { + return hres; + } + + rc = sqlite3_prepare_v2(db, + "INSERT INTO messages(id,owner,from_user,subject," + "body,received_at,read_flag,deleted_at,folder) " + "VALUES(?1,?2,?3,?4,?5,?6,0,0,?7);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, from_user, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, subject, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 5, body, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)now); + sqlite3_bind_int(stmt, 7, MAIL_FOLDER_INBOX); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + + return mail_sql_trim_inbox(db, owner, mail->max_messages); +} + +unsigned hybbx_mail_sql_purge_recycle(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *username) +{ + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + time_t cutoff; + int rc; + + if (storage == NULL || mail == NULL || username == NULL) { + return 0; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return 0; + } + + mail_sql_owner_norm(owner, sizeof(owner), username); + cutoff = time(NULL) - (time_t)mail->recycle_days * 86400; + + rc = sqlite3_prepare_v2(db, + "DELETE FROM messages WHERE owner=?1 AND folder=?2 " + "AND deleted_at > 0 AND deleted_at < ?3;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return 0; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_RECYCLE); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cutoff); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? (unsigned)sqlite3_changes(db) : 0; +} + +static hybbx_result_t mail_sql_resolve_index(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index, + uint64_t *out_id) +{ + const hybbx_mail_config_t *mail; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count; + hybbx_result_t rc; + + mail = hybbx_service_get_mail(service); + if (mail == NULL || !mail->enabled) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = hybbx_mail_sql_load_inbox(hybbx_service_get_storage(service), + hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (rc != HYBBX_OK) { + return rc; + } + + if (list_index == 0 || list_index > count) { + return HYBBX_ERR_NOT_FOUND; + } + + *out_id = entries[list_index - 1].id; + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_sql_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + hybbx_storage_t *storage; + sqlite3 *db; + sqlite3_stmt *stmt; + uint64_t id; + char owner[HYBBX_USER_NAME_MAX]; + char line[HYBBX_LINE_MAX]; + int rc; + hybbx_result_t hres; + const char *from_user; + const char *subject; + const char *body; + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + hres = mail_sql_resolve_index(service, session, list_index, &id); + if (hres == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + if (hres != HYBBX_OK) { + return hres; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), hybbx_session_username(session)); + + rc = sqlite3_prepare_v2(db, + "SELECT from_user,subject,body FROM messages " + "WHERE id=?1 AND owner=?2 AND folder=?3;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 3, MAIL_FOLDER_INBOX); + rc = sqlite3_step(stmt); + if (rc != SQLITE_ROW) { + sqlite3_finalize(stmt); + hybbx_session_write_line(session, "Cannot open message."); + return HYBBX_ERR_IO; + } + + from_user = (const char *)sqlite3_column_text(stmt, 0); + subject = (const char *)sqlite3_column_text(stmt, 1); + body = (const char *)sqlite3_column_text(stmt, 2); + if (from_user == NULL) { + from_user = ""; + } + if (subject == NULL) { + subject = ""; + } + if (body == NULL) { + body = ""; + } + + snprintf(line, sizeof(line), "From: %s", from_user); + hybbx_session_write_line(session, line); + snprintf(line, sizeof(line), "Subject: %s", subject); + hybbx_session_write_line(session, line); + + { + const char *p = body; + const char *nl; + + while (*p != '\0') { + nl = strchr(p, '\n'); + if (nl == NULL) { + hybbx_session_write_line(session, p); + break; + } + if (nl > p) { + size_t chunk = (size_t)(nl - p); + + if (chunk >= sizeof(line)) { + chunk = sizeof(line) - 1; + } + memcpy(line, p, chunk); + line[chunk] = '\0'; + hybbx_session_write_line(session, line); + } else { + hybbx_session_write_line(session, ""); + } + p = nl + 1; + } + } + + sqlite3_finalize(stmt); + + rc = sqlite3_prepare_v2(db, + "UPDATE messages SET read_flag=1 " + "WHERE id=?1 AND owner=?2;", + -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + (void)sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_sql_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + sqlite3 *db; + sqlite3_stmt *stmt; + hybbx_mail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + char owner[HYBBX_USER_NAME_MAX]; + size_t count; + size_t i; + unsigned moved = 0; + time_t now = time(NULL); + int rc; + hybbx_result_t hres; + + mail = hybbx_service_get_mail(service); + storage = hybbx_service_get_storage(service); + if (mail == NULL || !mail->enabled || storage == NULL) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (to == 0) { + to = from; + } + + (void)hybbx_mail_sql_purge_recycle(storage, mail, + hybbx_session_username(session)); + + hres = hybbx_mail_sql_load_inbox(storage, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, &count); + if (hres != HYBBX_OK) { + return hres; + } + + if (from > count || to > count) { + hybbx_session_write_line(session, "No such message."); + return HYBBX_OK; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), hybbx_session_username(session)); + + rc = sqlite3_prepare_v2(db, + "UPDATE messages SET folder=?4, deleted_at=?3 " + "WHERE id=?1 AND owner=?2 AND folder=?5;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)entries[i].id); + sqlite3_bind_text(stmt, 2, owner, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)now); + sqlite3_bind_int(stmt, 4, MAIL_FOLDER_RECYCLE); + sqlite3_bind_int(stmt, 5, MAIL_FOLDER_INBOX); + rc = sqlite3_step(stmt); + sqlite3_reset(stmt); + if (rc == SQLITE_DONE && sqlite3_changes(db) > 0) { + moved++; + } + } + + sqlite3_finalize(stmt); + + if (moved == 0) { + hybbx_session_write_line(session, "Delete failed."); + return HYBBX_ERR_IO; + } + + if (moved == 1) { + hybbx_session_write_line(session, + "Message moved to recycle (auto-purge after configured days)."); + } else { + char buf[64]; + + snprintf(buf, sizeof(buf), + "%u messages moved to recycle (auto-purge after %u days).", + moved, mail->recycle_days); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_mail_sql_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + const hybbx_mail_config_t *mail; + hybbx_storage_t *storage; + sqlite3 *db; + sqlite3_stmt *stmt; + char owner[HYBBX_USER_NAME_MAX]; + int rc; + char buf[64]; + + mail = hybbx_service_get_mail(service); + storage = hybbx_service_get_storage(service); + if (mail == NULL || !mail->enabled || storage == NULL) { + return HYBBX_ERR_UNSUPPORTED; + } + + db = hybbx_storage_sql_mail_db(storage); + if (db == NULL) { + return HYBBX_ERR_IO; + } + + mail_sql_owner_norm(owner, sizeof(owner), hybbx_session_username(session)); + (void)hybbx_mail_sql_purge_recycle(storage, mail, owner); + + rc = sqlite3_prepare_v2(db, + "DELETE FROM messages WHERE owner=?1 AND folder=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, owner, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 2, MAIL_FOLDER_RECYCLE); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + hybbx_session_write_line(session, "Cannot read recycle bin."); + return HYBBX_ERR_IO; + } + + if (sqlite3_changes(db) == 0) { + hybbx_session_write_line(session, "Recycle bin empty."); + } else { + snprintf(buf, sizeof(buf), "Recycle bin emptied (%d message(s)).", + sqlite3_changes(db)); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +#else /* HYBBX_HAVE_SQLITE */ + +hybbx_result_t hybbx_mail_sql_load_inbox(hybbx_storage_t *storage, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + (void)storage; + (void)username; + (void)entries; + (void)max_entries; + (void)out_count; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_deliver(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *from_user, + const char *to_user, + const char *subject, + const char *body) +{ + (void)storage; + (void)mail; + (void)from_user; + (void)to_user; + (void)subject; + (void)body; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + (void)service; + (void)session; + (void)list_index; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, + unsigned to) +{ + (void)service; + (void)session; + (void)from; + (void)to; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_mail_sql_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + (void)service; + (void)session; + return HYBBX_ERR_UNSUPPORTED; +} + +unsigned hybbx_mail_sql_purge_recycle(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *username) +{ + (void)storage; + (void)mail; + (void)username; + return 0; +} + +#endif /* HYBBX_HAVE_SQLITE */ diff --git a/src/core/mail_sql.h b/src/core/mail_sql.h new file mode 100644 index 0000000..cddd856 --- /dev/null +++ b/src/core/mail_sql.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_MAIL_SQL_H +#define HYBBX_MAIL_SQL_H + +#include "hybbx/mail.h" +#include "hybbx/storage.h" +#include "hybbx/types.h" + +struct hybbx_service; +struct hybbx_session; + +hybbx_result_t hybbx_mail_sql_load_inbox(hybbx_storage_t *storage, + const char *username, + hybbx_mail_entry_t *entries, + size_t max_entries, + size_t *out_count); + +hybbx_result_t hybbx_mail_sql_deliver(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *from_user, + const char *to_user, + const char *subject, + const char *body); + +hybbx_result_t hybbx_mail_sql_read(struct hybbx_service *service, + struct hybbx_session *session, + unsigned list_index); + +hybbx_result_t hybbx_mail_sql_delete_range(struct hybbx_service *service, + struct hybbx_session *session, + unsigned from, + unsigned to); + +hybbx_result_t hybbx_mail_sql_recycle_empty(struct hybbx_service *service, + struct hybbx_session *session); + +unsigned hybbx_mail_sql_purge_recycle(hybbx_storage_t *storage, + const hybbx_mail_config_t *mail, + const char *username); + +#endif /* HYBBX_MAIL_SQL_H */ diff --git a/src/core/mains_proxy.c b/src/core/mains_proxy.c new file mode 100644 index 0000000..f3475fe --- /dev/null +++ b/src/core/mains_proxy.c @@ -0,0 +1,1034 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/mains_proxy.h" +#include "hybbx/service.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/proxymail.h" +#include "hybbx/proxychat.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include + +/* Inter-node mesh: hybbx_circuit_link_connect() only — never peer Main TCP. */ + +#define MAINS_PROXY_RECONNECT_MS 5000u +#define MAINS_PROXY_CONNECT_ATTEMPTS 30u +#define MAINS_PROXY_DEFAULT_TTL 8u + +typedef struct mains_proxy_peer_runtime { + hybbx_mains_proxy_peer_config_t config; + int fd; + hybbx_circuit_decoder_t dec; + unsigned reconnect_ms; +} mains_proxy_peer_runtime_t; + +typedef struct mains_proxy_runtime { + hybbx_service_t *service; + hybbx_mains_proxy_mesh_t mesh; + mains_proxy_peer_runtime_t peers[HYBBX_MAINS_PROXY_MAX_PEERS]; + unsigned live_links; +} mains_proxy_runtime_t; + +static mains_proxy_runtime_t g_rt; + +static const char *mains_proxy_find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (scratch != NULL && scratch_len > 0) { + scratch[0] = '\0'; + } + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *line = cursor; + const char *eq; + const char *end = cursor; + + /* + * Config sections are serialized with ';' between key=value pairs + * (hybbx_config_format_section) but the parser used to look for '\n' + * only. Accept both separators so a single section can be parsed + * regardless of how it was produced. + */ + while (*end != '\0' && *end != ';' && *end != '\n') { + end++; + } + + if (line[0] == '[') { + cursor = (*end == '\0') ? end : end + 1; + continue; + } + + eq = strchr(line, '='); + if (eq == NULL || eq >= end) { + cursor = (*end == '\0') ? end : end + 1; + continue; + } + + if ((size_t)(eq - line) == key_len && + strncmp(line, key, key_len) == 0) { + const char *value = eq + 1; + + while (value < end && (*value == ' ' || *value == '\t')) { + value++; + } + + if (scratch != NULL && scratch_len > 0) { + size_t vlen = (size_t)(end - value); + + while (vlen > 0 && + (value[vlen - 1] == ' ' || value[vlen - 1] == '\t' || + value[vlen - 1] == '\r')) { + vlen--; + } + + if (vlen >= scratch_len) { + vlen = scratch_len - 1; + } + memcpy(scratch, value, vlen); + scratch[vlen] = '\0'; + return scratch; + } + + return value; + } + + cursor = (*end == '\0') ? end : end + 1; + } + + return NULL; +} + +static int str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int proxy_payload_get_line(const char *payload, size_t len, + const char *key, char *out, size_t out_len) +{ + size_t key_len; + const char *cursor; + const char *end; + + if (payload == NULL || key == NULL || out == NULL || out_len == 0) { + return 0; + } + + out[0] = '\0'; + key_len = strlen(key); + cursor = payload; + end = payload + len; + + while (cursor < end) { + const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor)); + size_t line_len; + + if (line_end == NULL) { + line_end = end; + } + + line_len = (size_t)(line_end - cursor); + if (line_len >= key_len + 1 && memcmp(cursor, key, key_len) == 0 && + cursor[key_len] == '=') { + const char *value = cursor + key_len + 1; + size_t vlen = line_len - key_len - 1; + + while (vlen > 0 && (*value == ' ' || *value == '\t')) { + value++; + vlen--; + } + if (vlen >= out_len) { + vlen = out_len - 1; + } + memcpy(out, value, vlen); + out[vlen] = '\0'; + return 1; + } + + if (line_end >= end) { + break; + } + cursor = line_end + 1; + } + + return 0; +} + +static const char *proxy_payload_body(const char *payload, size_t len, + size_t *body_len) +{ + size_t off; + + if (payload == NULL || body_len == NULL) { + return NULL; + } + + *body_len = 0; + + if (len >= 5 && memcmp(payload, "---\r\n", 5) == 0) { + off = 5; + } else if (len >= 4 && memcmp(payload, "---\n", 4) == 0) { + off = 4; + } else { + return NULL; + } + + while (off < len && (payload[off] == '\r' || payload[off] == '\n')) { + off++; + } + + *body_len = len - off; + return payload + off; +} + +static unsigned proxy_payload_get_ttl(const char *payload, size_t len) +{ + char scratch[HYBBX_CONFIG_LINE_MAX]; + const char *value; + unsigned long ttl; + + if (payload == NULL || len == 0 || len >= sizeof(scratch)) { + return 0; + } + + memcpy(scratch, payload, len); + scratch[len] = '\0'; + + value = NULL; + if (proxy_payload_get_line(scratch, len, "ttl", scratch, sizeof(scratch))) { + value = scratch; + } + + if (value == NULL || value[0] == '\0') { + return 0; + } + + ttl = strtoul(value, NULL, 10); + if (ttl > 255u) { + ttl = 255u; + } + return (unsigned)ttl; +} + +static size_t proxy_build_mail_payload(char *out, size_t out_cap, + const char *from_address, + const char *to_address, + const char *subject, + const char *body, + unsigned ttl) +{ + int n; + + if (out == NULL || out_cap == 0 || from_address == NULL || + to_address == NULL || subject == NULL || body == NULL) { + return 0; + } + + n = snprintf(out, out_cap, "from=%s\nto=%s\nsub=%s\nttl=%u\n---\n%s", + from_address, to_address, subject, ttl, body); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + return (size_t)n; +} + +static size_t proxy_build_chat_payload(char *out, size_t out_cap, + const char *from_address, + const char *line, + unsigned ttl) +{ + int n; + + if (out == NULL || out_cap == 0 || from_address == NULL || + line == NULL) { + return 0; + } + + n = snprintf(out, out_cap, "from=%s\nttl=%u\n---\n%s", from_address, ttl, line); + if (n < 0 || (size_t)n >= out_cap) { + return 0; + } + + return (size_t)n; +} + +static void proxy_dispatch_payload(hybbx_service_t *service, + hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len) +{ + char from[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char to[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char subj[HYBBX_MAIL_SUBJECT_MAX + 1]; + const char *body; + size_t body_len; + char scratch[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + + if (service == NULL || payload == NULL || len == 0 || + len > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return; + } + + memcpy(scratch, payload, len); + scratch[len] = '\0'; + + if (!proxy_payload_get_line(scratch, len, "from", from, sizeof(from))) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_MAIL) { + if (!proxy_payload_get_line(scratch, len, "to", to, sizeof(to))) { + return; + } + if (!proxy_payload_get_line(scratch, len, "sub", subj, sizeof(subj))) { + subj[0] = '\0'; + } + body = proxy_payload_body(scratch, len, &body_len); + if (body == NULL) { + return; + } + scratch[0] = '\0'; + if (body_len > 0) { + if (body_len >= sizeof(scratch)) { + body_len = sizeof(scratch) - 1; + } + memcpy(scratch, body, body_len); + scratch[body_len] = '\0'; + } + hybbx_proxymail_receive(service, from, to, subj, scratch); + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + body = proxy_payload_body(scratch, len, &body_len); + if (body == NULL) { + return; + } + scratch[0] = '\0'; + if (body_len > 0) { + if (body_len >= sizeof(scratch)) { + body_len = sizeof(scratch) - 1; + } + memcpy(scratch, body, body_len); + scratch[body_len] = '\0'; + } + hybbx_proxychat_receive(service, from, scratch); + } +} + +static hybbx_result_t proxy_peer_send(int fd, hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + + if (fd < 0 || payload == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode(proto, HYBBX_CIRCUIT_FLAG_NONE, + payload, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(fd, frame, frame_len); +} + +static void proxy_peer_disconnect(mains_proxy_peer_runtime_t *peer) +{ + if (peer == NULL) { + return; + } + + if (peer->fd >= 0) { + close(peer->fd); + peer->fd = -1; + } + + if (g_rt.live_links > 0) { + g_rt.live_links--; + } + + peer->reconnect_ms = MAINS_PROXY_RECONNECT_MS; +} + +static void on_mesh_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + mains_proxy_peer_runtime_t *from_peer = (mains_proxy_peer_runtime_t *)userdata; + char relay_buf[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + unsigned ttl; + unsigned i; + + (void)flags; + + if (g_rt.service == NULL || payload == NULL || len == 0 || + len > HYBBX_CIRCUIT_MAX_PAYLOAD) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_PROXY_MAIL && + proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + return; + } + + /* Always deliver locally first. */ + proxy_dispatch_payload(g_rt.service, proto, payload, len); + + /* Relay chat to all other live peers with TTL loop prevention. */ + if (proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + return; + } + + ttl = proxy_payload_get_ttl((const char *)payload, len); + if (ttl <= 1) { + return; + } + ttl--; + + if (len >= sizeof(relay_buf)) { + return; + } + memcpy(relay_buf, payload, len); + relay_buf[len] = '\0'; + + { + char ttl_str[32]; + char *ttl_pos; + int n; + + n = snprintf(ttl_str, sizeof(ttl_str), "ttl=%u", ttl); + if (n < 0 || (size_t)n >= sizeof(ttl_str)) { + return; + } + + ttl_pos = strstr(relay_buf, "\nttl="); + if (ttl_pos == NULL) { + /* + * Also accept ttl= at the very beginning of the payload + * (proxy_build_chat_payload puts it as the second line). + */ + if (strncmp(relay_buf, "ttl=", 4) == 0) { + ttl_pos = relay_buf; + } else { + /* No TTL field; drop rather than inject into a foreign payload. */ + return; + } + } else { + /* Point at the 't' of "ttl=", not the leading newline. */ + ttl_pos++; + } + + { + char *start = ttl_pos + 4; + char *end = start; + size_t old_len; + size_t new_len; + size_t tail_len; + + if (start == ttl_pos || *start == '\0' || *start == '\n' || + *start == '\r') { + return; + } + + /* Find end of current ttl line. */ + while (*end != '\0' && *end != '\n' && *end != '\r') { + end++; + } + + old_len = (size_t)(end - start); + new_len = strlen(ttl_str + 4); + tail_len = strlen(end); + + /* + * New value is shorter or equal because TTL only decreases, + * so in-place replacement with memmove is safe. + */ + if (new_len != old_len) { + memmove(start + new_len, end, tail_len + 1); + } + memcpy(start, ttl_str + 4, new_len); + } + } + + for (i = 0; i < g_rt.mesh.peer_count; i++) { + mains_proxy_peer_runtime_t *target = &g_rt.peers[i]; + + if (target->fd < 0) { + continue; + } + if (target == from_peer) { + continue; + } + (void)proxy_peer_send(target->fd, proto, + (const uint8_t *)relay_buf, strlen(relay_buf)); + } +} + +static int peer_name_matches(const hybbx_mains_proxy_peer_config_t *peer, + const char *remote_service) +{ + if (peer == NULL || remote_service == NULL || remote_service[0] == '\0') { + return 0; + } + + if (peer->peer_id[0] != '\0' && + str_ieq_local(peer->peer_id, remote_service)) { + return 1; + } + + if (peer->link_id[0] != '\0' && + str_ieq_local(peer->link_id, remote_service)) { + return 1; + } + + return 0; +} + +static mains_proxy_peer_runtime_t *proxy_find_peer_for_service( + const char *remote_service) +{ + unsigned i; + + for (i = 0; i < g_rt.mesh.peer_count; i++) { + if (g_rt.peers[i].fd < 0) { + continue; + } + if (peer_name_matches(&g_rt.peers[i].config, remote_service)) { + return &g_rt.peers[i]; + } + } + + return NULL; +} + +static hybbx_result_t proxy_peer_connect(mains_proxy_peer_runtime_t *peer) +{ + const hybbx_mains_proxy_peer_config_t *cfg; + const char *host; + unsigned port; + const char *link_id; + const char *peer_label; + unsigned attempt; + hybbx_result_t rc; + + if (peer == NULL) { + return HYBBX_ERR_INVALID; + } + + cfg = &peer->config; + peer_label = cfg->peer_id[0] != '\0' ? cfg->peer_id : "(unnamed)"; + + if (!cfg->enabled) { + return HYBBX_OK; + } + + if (cfg->wire == HYBBX_MAINS_PROXY_WIRE_AX25) { + hybbx_log_warn("[mains_proxy] peer '%s': wire=ax25 not active — use circuit", + peer_label); + return HYBBX_ERR_UNSUPPORTED; + } + + if (cfg->circuit_host[0] == '\0') { + return HYBBX_OK; + } + + if (cfg->link_id[0] == '\0' || cfg->link_password[0] == '\0') { + hybbx_log_warn("[mains_proxy] peer '%s' missing link_id or link_password", + peer_label); + return HYBBX_ERR_INVALID; + } + + host = cfg->circuit_host; + port = cfg->circuit_port; + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + + link_id = cfg->link_id; + + for (attempt = 0; attempt < MAINS_PROXY_CONNECT_ATTEMPTS; attempt++) { + rc = hybbx_circuit_link_connect(host, port, &peer->fd); + if (rc != HYBBX_OK) { + usleep(100000); + continue; + } + + hybbx_circuit_decoder_init(&peer->dec); + rc = hybbx_circuit_link_authenticate(peer->fd, cfg->link_password, + "proxy", link_id); + if (rc != HYBBX_OK) { + close(peer->fd); + peer->fd = -1; + usleep(100000); + continue; + } + + g_rt.live_links++; + peer->reconnect_ms = 0; + hybbx_log_info("[mains_proxy] linked to peer %s via HBX %s:%u link_id=%s", + peer_label, host, port, link_id); + return HYBBX_OK; + } + + hybbx_log_warn("[mains_proxy] could not link peer '%s' at %s:%u", + peer_label, host, port); + return HYBBX_ERR_IO; +} + +hybbx_mains_proxy_wire_t hybbx_mains_proxy_wire_parse(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_MAINS_PROXY_WIRE_CIRCUIT; + } + + if (str_ieq_local(value, "circuit") || str_ieq_local(value, "tcp") || + str_ieq_local(value, "tcpip") || str_ieq_local(value, "tcp/ip")) { + return HYBBX_MAINS_PROXY_WIRE_CIRCUIT; + } + + if (str_ieq_local(value, "ax25") || str_ieq_local(value, "packet_radio")) { + return HYBBX_MAINS_PROXY_WIRE_AX25; + } + + return HYBBX_MAINS_PROXY_WIRE_CIRCUIT; +} + +const char *hybbx_mains_proxy_wire_name(hybbx_mains_proxy_wire_t wire) +{ + switch (wire) { + case HYBBX_MAINS_PROXY_WIRE_AX25: + return "ax25"; + case HYBBX_MAINS_PROXY_WIRE_CIRCUIT: + default: + return "circuit"; + } +} + +hybbx_mains_proxy_duplex_t hybbx_mains_proxy_duplex_parse(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_MAINS_PROXY_DUPLEX_FULL; + } + + if (str_ieq_local(value, "half") || str_ieq_local(value, "half-duplex") || + str_ieq_local(value, "half_duplex")) { + return HYBBX_MAINS_PROXY_DUPLEX_HALF; + } + + return HYBBX_MAINS_PROXY_DUPLEX_FULL; +} + +const char *hybbx_mains_proxy_duplex_name(hybbx_mains_proxy_duplex_t duplex) +{ + return duplex == HYBBX_MAINS_PROXY_DUPLEX_HALF ? "half" : "full"; +} + +void hybbx_mains_proxy_peer_defaults(hybbx_mains_proxy_peer_config_t *peer) +{ + if (peer == NULL) { + return; + } + + memset(peer, 0, sizeof(*peer)); + peer->circuit_port = HYBBX_CIRCUIT_DEFAULT_PORT; + peer->wire = HYBBX_MAINS_PROXY_WIRE_CIRCUIT; + peer->duplex = HYBBX_MAINS_PROXY_DUPLEX_FULL; + peer->use_secondary = 1; + peer->enabled = 1; +} + +hybbx_result_t hybbx_mains_proxy_peer_parse(const char *config, + hybbx_mains_proxy_peer_config_t *out) +{ + char scratch[HYBBX_CONFIG_LINE_MAX]; + const char *value; + int legacy_host = 0; + int legacy_port = 0; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_mains_proxy_peer_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = mains_proxy_find_kv(config, "enabled", scratch, sizeof(scratch)); + if (value != NULL) { + out->enabled = hybbx_parse_bool(value, 1); + } + + value = mains_proxy_find_kv(config, "peer_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->peer_id, value, sizeof(out->peer_id)); + } + + value = mains_proxy_find_kv(config, "circuit_host", scratch, + sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->circuit_host, value, sizeof(out->circuit_host)); + } + + value = mains_proxy_find_kv(config, "circuit_port", scratch, + sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned long port = strtoul(value, NULL, 10); + + if (port >= 1u && port <= 65535u) { + out->circuit_port = (unsigned)port; + } + } + + value = mains_proxy_find_kv(config, "link_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->link_id, value, sizeof(out->link_id)); + } + + value = mains_proxy_find_kv(config, "link_password", scratch, + sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->link_password, value, sizeof(out->link_password)); + } + + value = mains_proxy_find_kv(config, "host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->host, value, sizeof(out->host)); + legacy_host = 1; + } + + value = mains_proxy_find_kv(config, "port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned long port = strtoul(value, NULL, 10); + + if (port >= 1u && port <= 65535u) { + out->port = (unsigned)port; + } + legacy_port = 1; + } + + if (legacy_host && out->circuit_host[0] == '\0') { + hybbx_strlcpy(out->circuit_host, out->host, + sizeof(out->circuit_host)); + hybbx_log_warn("[mains_proxy] peer '%s': deprecated key host= — use " + "circuit_host= (mapped for now)", + out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)"); + } else if (legacy_host) { + hybbx_log_warn("[mains_proxy] peer '%s': deprecated key host= ignored " + "(circuit_host set)", + out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)"); + } + + if (legacy_port) { + hybbx_log_warn("[mains_proxy] peer '%s': deprecated key port= ignored — " + "use circuit_port= (HBX hub, default %u)", + out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)", + (unsigned)HYBBX_CIRCUIT_DEFAULT_PORT); + } + + value = mains_proxy_find_kv(config, "wire", scratch, sizeof(scratch)); + out->wire = hybbx_mains_proxy_wire_parse(value); + + value = mains_proxy_find_kv(config, "duplex", scratch, sizeof(scratch)); + out->duplex = hybbx_mains_proxy_duplex_parse(value); + + value = mains_proxy_find_kv(config, "use_secondary", scratch, + sizeof(scratch)); + if (value != NULL) { + out->use_secondary = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} + +void hybbx_mains_proxy_mesh_init(hybbx_mains_proxy_mesh_t *mesh) +{ + if (mesh == NULL) { + return; + } + + memset(mesh, 0, sizeof(*mesh)); +} + +hybbx_result_t hybbx_mains_proxy_mesh_start(hybbx_service_t *service, + hybbx_mains_proxy_mesh_t *mesh) +{ + unsigned i; + unsigned configured = 0; + unsigned linked = 0; + + if (mesh == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_mains_proxy_mesh_stop(mesh); + + memset(&g_rt, 0, sizeof(g_rt)); + g_rt.service = service; + g_rt.mesh = *mesh; + + for (i = 0; i < mesh->peer_count; i++) { + g_rt.peers[i].config = mesh->peers[i]; + g_rt.peers[i].fd = -1; + hybbx_circuit_decoder_init(&g_rt.peers[i].dec); + + if (!mesh->peers[i].enabled) { + continue; + } + + configured++; + + if (mesh->peers[i].circuit_host[0] == '\0') { + continue; + } + + if (proxy_peer_connect(&g_rt.peers[i]) == HYBBX_OK && + g_rt.peers[i].fd >= 0) { + linked++; + } + } + + if (configured == 0) { + hybbx_log_warn("[mains_proxy] no active peers configured"); + return HYBBX_ERR_NOT_FOUND; + } + + mesh->running = 1; + g_rt.mesh.running = 1; + + hybbx_log_info("[mains_proxy] mesh started (%u peer(s) configured, %u HBX link(s))", + configured, linked); + + return HYBBX_OK; +} + +void hybbx_mains_proxy_mesh_stop(hybbx_mains_proxy_mesh_t *mesh) +{ + unsigned i; + + if (mesh != NULL && mesh->running) { + hybbx_log_info("[mains_proxy] mesh stopped"); + mesh->running = 0; + } + + for (i = 0; i < HYBBX_MAINS_PROXY_MAX_PEERS; i++) { + proxy_peer_disconnect(&g_rt.peers[i]); + } + + memset(&g_rt, 0, sizeof(g_rt)); +} + +void hybbx_mains_proxy_mesh_tick(hybbx_service_t *service, + hybbx_mains_proxy_mesh_t *mesh) +{ + unsigned i; + static unsigned tick_ms; + + (void)service; + + if (mesh == NULL || !mesh->running) { + return; + } + + tick_ms += 50; + if (tick_ms >= 1000) { + tick_ms = 0; + } + + for (i = 0; i < mesh->peer_count; i++) { + mains_proxy_peer_runtime_t *peer = &g_rt.peers[i]; + struct pollfd pfd; + uint8_t buf[512]; + size_t read_len; + hybbx_result_t rc; + int pr; + + if (!peer->config.enabled) { + continue; + } + + if (peer->fd < 0) { + if (peer->config.circuit_host[0] == '\0') { + continue; + } + if (peer->reconnect_ms > 0) { + if (tick_ms != 0) { + continue; + } + if (peer->reconnect_ms > 50) { + peer->reconnect_ms -= 50; + continue; + } + peer->reconnect_ms = 0; + } + (void)proxy_peer_connect(peer); + continue; + } + + pfd.fd = peer->fd; + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, 0); + if (pr <= 0) { + continue; + } + + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + hybbx_log_stats("[mains_proxy] peer '%s' disconnected", + peer->config.peer_id[0] != '\0' ? peer->config.peer_id + : "(unnamed)"); + proxy_peer_disconnect(peer); + continue; + } + + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + rc = hybbx_circuit_link_read(peer->fd, buf, sizeof(buf), &read_len); + if (rc != HYBBX_OK || read_len == 0) { + proxy_peer_disconnect(peer); + continue; + } + + hybbx_circuit_decoder_feed(&peer->dec, buf, read_len, + on_mesh_frame, peer); + } +} + +int hybbx_mains_proxy_mesh_active(void) +{ + return g_rt.mesh.running && g_rt.live_links > 0; +} + +hybbx_result_t hybbx_mains_proxy_send_mail(hybbx_service_t *service, + const char *from_address, + const char *to_address, + const char *subject, + const char *body) +{ + char user[HYBBX_USER_NAME_MAX]; + char remote[HYBBX_PROXYMAIL_SERVICE_NAME_MAX]; + char payload[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + size_t payload_len; + mains_proxy_peer_runtime_t *peer; + + (void)service; + + if (!g_rt.mesh.running || g_rt.live_links == 0) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (!hybbx_proxymail_parse_address(to_address, user, sizeof(user), + remote, sizeof(remote))) { + return HYBBX_ERR_INVALID; + } + + peer = proxy_find_peer_for_service(remote); + if (peer == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + payload_len = proxy_build_mail_payload(payload, sizeof(payload), + from_address, to_address, + subject != NULL ? subject : "", + body != NULL ? body : "", + MAINS_PROXY_DEFAULT_TTL); + if (payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + return proxy_peer_send(peer->fd, HYBBX_CIRCUIT_PROTO_PROXY_MAIL, + (const uint8_t *)payload, payload_len); +} + +hybbx_result_t hybbx_mains_proxy_send_chat(hybbx_service_t *service, + const char *from_address, + const char *line) +{ + char payload[HYBBX_CIRCUIT_MAX_PAYLOAD + 1]; + size_t payload_len; + unsigned i; + hybbx_result_t rc = HYBBX_ERR_NOT_FOUND; + int sent = 0; + + (void)service; + + if (!g_rt.mesh.running || g_rt.live_links == 0 || line == NULL || + line[0] == '\0') { + return HYBBX_ERR_UNSUPPORTED; + } + + payload_len = proxy_build_chat_payload(payload, sizeof(payload), + from_address, line, + MAINS_PROXY_DEFAULT_TTL); + if (payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < g_rt.mesh.peer_count; i++) { + if (g_rt.peers[i].fd < 0) { + continue; + } + + if (proxy_peer_send(g_rt.peers[i].fd, HYBBX_CIRCUIT_PROTO_PROXY_CHAT, + (const uint8_t *)payload, + payload_len) == HYBBX_OK) { + sent = 1; + rc = HYBBX_OK; + } + } + + return sent ? rc : HYBBX_ERR_NOT_FOUND; +} + +void hybbx_mains_proxy_inbound_frame(hybbx_service_t *service, + hybbx_circuit_proto_t proto, + const uint8_t *payload, size_t len) +{ + if (service == NULL || payload == NULL || len == 0) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_PROXY_MAIL && + proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + return; + } + + proxy_dispatch_payload(service, proto, payload, len); +} diff --git a/src/core/max25.c b/src/core/max25.c new file mode 100644 index 0000000..2770102 --- /dev/null +++ b/src/core/max25.c @@ -0,0 +1,507 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/max25.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include +#include +#include +#include + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include +#include +#include +#include +#include +#include +#endif + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +static const char *find_prefixed_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + char prefixed[HYBBX_CONFIG_KEY_MAX + 8]; + + snprintf(prefixed, sizeof(prefixed), "max25_%s", key); + return find_kv(config, prefixed, scratch, scratch_len); +} + +void hybbx_max25_config_defaults(hybbx_max25_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->check = 1; + hybbx_strlcpy(cfg->host, HYBBX_MAX25_DEFAULT_HOST, sizeof(cfg->host)); + cfg->port = HYBBX_MAX25_DEFAULT_PORT; + cfg->timeout_ms = HYBBX_MAX25_PROBE_TIMEOUT_MS; +} + +void hybbx_max25_config_apply(hybbx_max25_config_t *cfg, + const hybbx_config_t *config) +{ + const char *host; + + if (cfg == NULL || config == NULL) { + return; + } + + hybbx_max25_config_defaults(cfg); + cfg->check = hybbx_config_get_bool(config, "max25", "check", 1); + host = hybbx_config_get(config, "max25", "host", HYBBX_MAX25_DEFAULT_HOST); + hybbx_strlcpy(cfg->host, host, sizeof(cfg->host)); + cfg->port = hybbx_config_get_uint(config, "max25", "port", + HYBBX_MAX25_DEFAULT_PORT, 1u, 65535u); + cfg->timeout_ms = hybbx_config_get_uint(config, "max25", "timeout_ms", + HYBBX_MAX25_PROBE_TIMEOUT_MS, + 100u, 60000u); +} + +void hybbx_max25_config_parse_kv(const char *max25_kv, + hybbx_max25_config_t *cfg) +{ + char scratch[HYBBX_CONFIG_VALUE_MAX]; + const char *value; + unsigned long n; + char *end; + + if (cfg == NULL) { + return; + } + + hybbx_max25_config_defaults(cfg); + if (max25_kv == NULL || max25_kv[0] == '\0') { + cfg->check = 0; + return; + } + + value = find_prefixed_kv(max25_kv, "check", scratch, sizeof(scratch)); + if (value != NULL) { + cfg->check = hybbx_parse_bool(value, 1); + } else { + cfg->check = 0; + } + + value = find_prefixed_kv(max25_kv, "host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(cfg->host, value, sizeof(cfg->host)); + } + + value = find_prefixed_kv(max25_kv, "port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + n = strtoul(value, &end, 10); + if (end != value && n > 0 && n <= 65535u) { + cfg->port = (unsigned)n; + } + } + + value = find_prefixed_kv(max25_kv, "timeout_ms", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + n = strtoul(value, &end, 10); + if (end != value && n >= 100 && n <= 60000) { + cfg->timeout_ms = (unsigned)n; + } + } +} + +const char *hybbx_max25_config_skip_prefix(const char *config, + hybbx_max25_config_t *cfg) +{ + char scratch[512]; + const char *sep; + size_t len; + + if (config == NULL || cfg == NULL) { + return config; + } + + sep = strchr(config, HYBBX_PACKET_RADIO_INSTANCE_SEP); + len = sep != NULL ? (size_t)(sep - config) : strlen(config); + if (len == 0 || len >= sizeof(scratch)) { + hybbx_max25_config_defaults(cfg); + cfg->check = 0; + return config; + } + + memcpy(scratch, config, len); + scratch[len] = '\0'; + + if (find_prefixed_kv(scratch, "check", scratch, sizeof(scratch)) == NULL) { + hybbx_max25_config_defaults(cfg); + cfg->check = 0; + return config; + } + + hybbx_max25_config_parse_kv(scratch, cfg); + return sep != NULL ? sep + 1 : config + len; +} + +void hybbx_max25_status_clear(hybbx_max25_status_t *status) +{ + if (status == NULL) { + return; + } + + memset(status, 0, sizeof(*status)); +} + +#if !defined(_WIN32) && !defined(__AMIGA__) + +static void max25_status_assign(char *dst, size_t dst_len, const char *value) +{ + if (dst == NULL || dst_len == 0) { + return; + } + + dst[0] = '\0'; + if (value == NULL || value[0] == '\0') { + return; + } + + hybbx_strlcpy(dst, value, dst_len); +} + +static void max25_status_parse_line(const char *line, + hybbx_max25_status_t *status) +{ + char buf[512]; + char *save = NULL; + char *tok; + const char *payload; + + if (status == NULL || line == NULL) { + return; + } + + if (strncmp(line, "STATUS ", 7) != 0) { + return; + } + + payload = line + 7; + if (strlen(payload) >= sizeof(buf)) { + return; + } + + hybbx_strlcpy(buf, payload, sizeof(buf)); + + for (tok = strtok_r(buf, " ", &save); tok != NULL; + tok = strtok_r(NULL, " ", &save)) { + char *eq = strchr(tok, '='); + + if (eq == NULL) { + continue; + } + + *eq = '\0'; + if (strcmp(tok, "error") == 0) { + max25_status_assign(status->error, sizeof(status->error), eq + 1); + } else if (strcmp(tok, "voice") == 0) { + max25_status_assign(status->voice, sizeof(status->voice), eq + 1); + } else if (strcmp(tok, "stack") == 0) { + max25_status_assign(status->stack, sizeof(status->stack), eq + 1); + } else if (strcmp(tok, "serial") == 0) { + max25_status_assign(status->serial, sizeof(status->serial), eq + 1); + } + } +} + +static int max25_read_line(int fd, char *buf, size_t buflen, + unsigned timeout_ms) +{ + size_t pos = 0; + unsigned elapsed = 0; + const unsigned step_ms = 50u; + + if (buf == NULL || buflen == 0) { + return -1; + } + + buf[0] = '\0'; + + while (pos + 1 < buflen && elapsed <= timeout_ms) { + struct pollfd pfd; + char ch; + ssize_t n; + + pfd.fd = fd; + pfd.events = POLLIN; + pfd.revents = 0; + + if (poll(&pfd, 1, (int)step_ms) <= 0) { + elapsed += step_ms; + continue; + } + + n = recv(fd, &ch, 1, 0); + if (n <= 0) { + return -1; + } + + if (ch == '\n') { + buf[pos] = '\0'; + while (pos > 0 && (buf[pos - 1] == '\r' || buf[pos - 1] == ' ')) { + buf[--pos] = '\0'; + } + return 0; + } + + if (ch != '\r') { + buf[pos++] = ch; + } + } + + return -1; +} + +static int max25_tcp_connect(const char *host, unsigned port, + unsigned timeout_ms, int *out_fd) +{ + struct sockaddr_in6 addr6; + struct sockaddr_in addr4; + struct pollfd pfd; + const struct sockaddr *addr; + socklen_t addr_len; + int fd; + int flags; + int rc; + int so_error = 0; + socklen_t so_len = sizeof(so_error); + + if (out_fd == NULL) { + return -1; + } + + *out_fd = -1; + + if (host == NULL || host[0] == '\0' || port == 0u) { + return -1; + } + + if (strchr(host, ':') != NULL) { + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + if (inet_pton(AF_INET6, host, &addr6.sin6_addr) != 1) { + return -1; + } + fd = socket(AF_INET6, SOCK_STREAM, 0); + addr = (const struct sockaddr *)&addr6; + addr_len = sizeof(addr6); + } else { + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &addr4.sin_addr) != 1) { + return -1; + } + fd = socket(AF_INET, SOCK_STREAM, 0); + addr = (const struct sockaddr *)&addr4; + addr_len = sizeof(addr4); + } + + if (fd < 0) { + return -1; + } + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + close(fd); + return -1; + } + + rc = connect(fd, addr, addr_len); + if (rc != 0 && errno != EINPROGRESS) { + close(fd); + return -1; + } + if (rc != 0) { + pfd.fd = fd; + pfd.events = POLLOUT; + pfd.revents = 0; + + rc = poll(&pfd, 1, (int)timeout_ms); + if (rc <= 0) { + close(fd); + return -1; + } + + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_len) != 0 || + so_error != 0) { + close(fd); + return -1; + } + } + + if (fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) != 0) { + close(fd); + return -1; + } + + *out_fd = fd; + return 0; +} + +static void max25_log_accepted_status(const hybbx_max25_config_t *cfg, + const hybbx_max25_status_t *status) +{ + const char *error = status->error[0] != '\0' ? status->error : "n/a"; + const char *voice = status->voice[0] != '\0' ? status->voice : "n/a"; + const char *stack = status->stack[0] != '\0' ? status->stack : "n/a"; + const char *serial = status->serial[0] != '\0' ? status->serial : "n/a"; + + hybbx_log_info("[max25] max25d at %s:%u — STATUS accepted " + "(error=%s voice=%s stack=%s serial=%s; MAX25 reporting only)", + cfg->host, cfg->port, error, voice, stack, serial); +} + +static hybbx_result_t max25_handshake(const hybbx_max25_config_t *cfg, + hybbx_max25_status_t *status) +{ + char line[512]; + int fd = -1; + + if (max25_tcp_connect(cfg->host, cfg->port, cfg->timeout_ms, &fd) != 0) { + return HYBBX_ERR_IO; + } + + if (max25_read_line(fd, line, sizeof(line), cfg->timeout_ms) != 0 || + strcmp(line, "OK") != 0) { + close(fd); + return HYBBX_ERR_IO; + } + + if (max25_read_line(fd, line, sizeof(line), cfg->timeout_ms) != 0 || + strncmp(line, "STATUS ", 7) != 0) { + close(fd); + return HYBBX_ERR_IO; + } + + max25_status_parse_line(line, status); + status->handshake_ok = 1; + close(fd); + return HYBBX_OK; +} + +#endif /* !WIN32 && !AMIGA */ + +hybbx_result_t hybbx_max25_probe(const hybbx_max25_config_t *cfg, + hybbx_max25_status_t *status_out) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)cfg; + if (status_out != NULL) { + hybbx_max25_status_clear(status_out); + } + return HYBBX_OK; +#else + hybbx_result_t rc; + + if (cfg == NULL || !cfg->check) { + if (status_out != NULL) { + hybbx_max25_status_clear(status_out); + } + return HYBBX_OK; + } + + if (cfg->host[0] == '\0' || cfg->port == 0u) { + return HYBBX_ERR_INVALID; + } + + if (status_out != NULL) { + hybbx_max25_status_clear(status_out); + } + + rc = max25_handshake(cfg, status_out); + if (rc != HYBBX_OK) { + hybbx_log_warn("[max25] max25d unreachable at %s:%u (timeout %u ms)", + cfg->host, cfg->port, cfg->timeout_ms); + return rc; + } + + if (status_out != NULL) { + max25_log_accepted_status(cfg, status_out); + } else { + hybbx_log_info("[max25] max25d reachable at %s:%u — M25/1 handshake OK", + cfg->host, cfg->port); + } + + return HYBBX_OK; +#endif +} + +hybbx_result_t hybbx_max25_wait_ready(const hybbx_max25_config_t *cfg) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)cfg; + return HYBBX_OK; +#else + hybbx_max25_status_t status; + unsigned waited_ms = 0; + const unsigned step_ms = 2000u; + + if (cfg == NULL || !cfg->check) { + return HYBBX_OK; + } + + while (hybbx_max25_probe(cfg, &status) != HYBBX_OK) { + if (waited_ms >= HYBBX_MAX25_PROBE_WAIT_MS) { + hybbx_log_warn("[max25] max25d unreachable at %s:%u after %u s " + "— start MAX25 or set [max25] check=no", + cfg->host, cfg->port, + HYBBX_MAX25_PROBE_WAIT_MS / 1000u); + return HYBBX_ERR_IO; + } + if (waited_ms == 0u) { + hybbx_log_info("[max25] waiting for max25d at %s:%u (up to %u s)", + cfg->host, cfg->port, + HYBBX_MAX25_PROBE_WAIT_MS / 1000u); + } + usleep((useconds_t)step_ms * 1000u); + waited_ms += step_ms; + } + + return HYBBX_OK; +#endif +} diff --git a/src/core/messages.c b/src/core/messages.c new file mode 100644 index 0000000..582d31d --- /dev/null +++ b/src/core/messages.c @@ -0,0 +1,110 @@ +#include "hybbx/messages.h" +#include "hybbx/session.h" +#include "hybbx/traffic.h" + +#include +#include + +static hybbx_result_t msg_format_prefixed(char *out, size_t out_len, + const char *prefix, + const char *from_label, + const char *body) +{ + int n; + + if (out == NULL || out_len == 0 || prefix == NULL || body == NULL) { + return HYBBX_ERR_INVALID; + } + + if (from_label != NULL && from_label[0] != '\0') { + n = snprintf(out, out_len, "%s%s: %s", prefix, from_label, body); + } else { + n = snprintf(out, out_len, "%s%s", prefix, body); + } + + if (n < 0 || (size_t)n >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_msg_format_system(char *out, size_t out_len, + const char *body) +{ + return msg_format_prefixed(out, out_len, HYBBX_MSG_PREFIX_SYSTEM, + NULL, body); +} + +hybbx_result_t hybbx_msg_format_sysop(char *out, size_t out_len, + const char *from_label, + const char *body) +{ + return msg_format_prefixed(out, out_len, HYBBX_MSG_PREFIX_SYSOP, + from_label, body); +} + +hybbx_result_t hybbx_msg_format_private(char *out, size_t out_len, + const char *from_label, + const char *body) +{ + return msg_format_prefixed(out, out_len, HYBBX_MSG_PREFIX_PRIVATE, + from_label, body); +} + +static hybbx_result_t msg_send_formatted(struct hybbx_session *session, + hybbx_result_t (*format_fn)(char *, + size_t, + const char *, + const char *), + const char *from_label, + const char *body) +{ + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (session == NULL || body == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = format_fn(line, sizeof(line), from_label, body); + if (rc != HYBBX_OK) { + return rc; + } + + return hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_msg_send_system(struct hybbx_session *session, + const char *body) +{ + char line[HYBBX_LINE_MAX]; + hybbx_result_t rc; + + if (session == NULL || body == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_msg_format_system(line, sizeof(line), body); + if (rc != HYBBX_OK) { + return rc; + } + + return hybbx_session_write_line(session, line); +} + +hybbx_result_t hybbx_msg_send_sysop(struct hybbx_session *session, + const char *from_label, + const char *body) +{ + return msg_send_formatted(session, hybbx_msg_format_sysop, + from_label, body); +} + +hybbx_result_t hybbx_msg_send_private(struct hybbx_session *session, + const char *from_label, + const char *body) +{ + return msg_send_formatted(session, hybbx_msg_format_private, + from_label, body); +} diff --git a/src/core/monitor.c b/src/core/monitor.c new file mode 100644 index 0000000..0b6d0c5 --- /dev/null +++ b/src/core/monitor.c @@ -0,0 +1,497 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/monitor.h" +#include "hybbx/auth.h" +#include "hybbx/config.h" +#include "hybbx/log.h" +#include "hybbx/security.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include + +#define MONITOR_LINE_MAX 512u +#define MONITOR_READ_CHUNK 4096u + +typedef struct monitor_tail { + char path[HYBBX_PATH_MAX]; + off_t offset; + int active; + char carry[MONITOR_LINE_MAX]; + size_t carry_len; +} monitor_tail_t; + +static hybbx_monitor_config_t g_mon_cfg; +static int g_mon_ready; +static monitor_tail_t g_tail_hybbx; +static monitor_tail_t g_tail_security; +static int g_any_monitor_active; + +void hybbx_monitor_config_defaults(hybbx_monitor_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + cfg->enabled = 1; + cfg->follow_hybbx = 1; + cfg->follow_security = 1; + cfg->invisible_sysop = 0; + cfg->invite_timeout_sec = 20u; +} + +static void monitor_parse_allow(hybbx_monitor_config_t *cfg, const char *raw) +{ + char buf[HYBBX_CONFIG_VALUE_MAX]; + char *p; + char *comma; + + cfg->allow_count = 0; + if (raw == NULL || raw[0] == '\0') { + return; + } + + hybbx_strlcpy(buf, raw, sizeof(buf)); + p = buf; + while (p != NULL && *p != '\0' && + cfg->allow_count < HYBBX_MONITOR_ALLOW_MAX) { + char *tok = p; + comma = strchr(p, ','); + if (comma != NULL) { + *comma = '\0'; + p = comma + 1; + } else { + p = NULL; + } + while (*tok == ' ' || *tok == '\t') { + tok++; + } + if (*tok == '\0') { + continue; + } + { + char *end = tok + strlen(tok); + while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) { + *--end = '\0'; + } + } + if (tok[0] == '\0') { + continue; + } + hybbx_strlcpy(cfg->allow[cfg->allow_count], tok, + sizeof(cfg->allow[cfg->allow_count])); + cfg->allow_count++; + } +} + +void hybbx_monitor_config_apply(const struct hybbx_config *config) +{ + const char *allow_raw; + + hybbx_monitor_shutdown(); + hybbx_monitor_config_defaults(&g_mon_cfg); + + if (config != NULL) { + g_mon_cfg.enabled = + hybbx_config_get_bool(config, "monitor", "enabled", 1); + g_mon_cfg.follow_hybbx = + hybbx_config_get_bool(config, "monitor", "follow_hybbx", 1); + g_mon_cfg.follow_security = + hybbx_config_get_bool(config, "monitor", "follow_security", 1); + /* Prefer hyphen key; accept underscore alias. */ + if (hybbx_config_get(config, "monitor", "invisible-sysop", NULL) != + NULL) { + g_mon_cfg.invisible_sysop = hybbx_config_get_bool( + config, "monitor", "invisible-sysop", 0); + } else { + g_mon_cfg.invisible_sysop = hybbx_config_get_bool( + config, "monitor", "invisible_sysop", 0); + } + g_mon_cfg.invite_timeout_sec = hybbx_config_get_uint( + config, "monitor", "invite_timeout_sec", 20u, 5u, 300u); + allow_raw = hybbx_config_get(config, "monitor", "allow", NULL); + monitor_parse_allow(&g_mon_cfg, allow_raw); + } + + g_mon_ready = 1; + hybbx_log_info("[monitor] enabled=%s follow_hybbx=%s follow_security=%s " + "invisible-sysop=%s invite_timeout_sec=%u allow=%u", + g_mon_cfg.enabled ? "yes" : "no", + g_mon_cfg.follow_hybbx ? "yes" : "no", + g_mon_cfg.follow_security ? "yes" : "no", + g_mon_cfg.invisible_sysop ? "yes" : "no", + g_mon_cfg.invite_timeout_sec, + g_mon_cfg.allow_count); +} + +const hybbx_monitor_config_t *hybbx_monitor_config_get(void) +{ + return g_mon_ready ? &g_mon_cfg : NULL; +} + +int hybbx_monitor_enabled(void) +{ + return g_mon_ready && g_mon_cfg.enabled; +} + +int hybbx_monitor_invisible_sysop(void) +{ + return g_mon_ready && g_mon_cfg.invisible_sysop; +} + +int hybbx_monitor_user_allowed(const char *username) +{ + unsigned i; + + if (!g_mon_ready || username == NULL || username[0] == '\0') { + return 0; + } + + for (i = 0; i < g_mon_cfg.allow_count; i++) { + if (strcasecmp(g_mon_cfg.allow[i], username) == 0) { + return 1; + } + } + return 0; +} + +int hybbx_monitor_session_may_use(const struct hybbx_session *session) +{ + const char *user; + + if (!hybbx_monitor_enabled() || session == NULL) { + return 0; + } + + if (hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + return 1; + } + + user = hybbx_session_username(session); + return hybbx_monitor_user_allowed(user); +} + +typedef struct mon_count_ctx { + unsigned count; +} mon_count_ctx_t; + +static void mon_count_visitor(hybbx_session_t *session, void *userdata) +{ + mon_count_ctx_t *ctx = (mon_count_ctx_t *)userdata; + + if (session != NULL && hybbx_monitor_is_active(session)) { + ctx->count++; + } +} + +static void monitor_refresh_active_flag(hybbx_service_t *service) +{ + mon_count_ctx_t ctx; + + ctx.count = 0; + if (service != NULL) { + hybbx_service_visit_sessions(service, mon_count_visitor, &ctx); + } + g_any_monitor_active = ctx.count > 0; +} + +static void monitor_tail_seek_end(monitor_tail_t *tail, const char *path) +{ + struct stat st; + + tail->path[0] = '\0'; + tail->offset = 0; + tail->active = 0; + + if (path == NULL || path[0] == '\0') { + return; + } + + hybbx_strlcpy(tail->path, path, sizeof(tail->path)); + if (stat(tail->path, &st) != 0) { + return; + } + + tail->offset = st.st_size; + tail->active = 1; +} + +static void monitor_reset_tails(void) +{ + char path[HYBBX_PATH_MAX]; + + memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx)); + memset(&g_tail_security, 0, sizeof(g_tail_security)); + + if (g_mon_cfg.follow_hybbx && + hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_hybbx, path); + } + if (g_mon_cfg.follow_security && + hybbx_security_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_security, path); + } +} + +hybbx_result_t hybbx_monitor_set_active(hybbx_session_t *session, int on) +{ + hybbx_service_t *service; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_monitor_enabled()) { + hybbx_session_write_line(session, "Monitor is disabled in config."); + return HYBBX_ERR_DENIED; + } + + hybbx_session_set_monitor_active(session, on ? 1 : 0); + service = hybbx_session_service(session); + monitor_refresh_active_flag(service); + + if (on) { + /* Fresh follow from current EOF — no backlog. */ + monitor_reset_tails(); + hybbx_session_write_line(session, "Monitor on."); + if (hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + if (hybbx_monitor_invisible_sysop()) { + hybbx_session_write_line(session, + "Sysop: hidden from /who (/monitor invisible-sysop=yes)."); + } else { + hybbx_session_write_line(session, + "Sysop: hidden from /who and /online while monitor is on."); + } + } + } else { + hybbx_session_write_line(session, "Monitor off."); + monitor_refresh_active_flag(service); + if (!g_any_monitor_active) { + memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx)); + memset(&g_tail_security, 0, sizeof(g_tail_security)); + } + } + + return HYBBX_OK; +} + +int hybbx_monitor_is_active(const hybbx_session_t *session) +{ + return hybbx_session_monitor_active(session); +} + +typedef struct mon_bcast_ctx { + const char *line; + hybbx_session_t *skip; +} mon_bcast_ctx_t; + +static void mon_bcast_visitor(hybbx_session_t *session, void *userdata) +{ + mon_bcast_ctx_t *ctx = (mon_bcast_ctx_t *)userdata; + char out[MONITOR_LINE_MAX + 16]; + + if (session == NULL || ctx == NULL || ctx->line == NULL) { + return; + } + if (session == ctx->skip) { + return; + } + if (!hybbx_monitor_is_active(session)) { + return; + } + + snprintf(out, sizeof(out), "[monitor] %s", ctx->line); + hybbx_session_write_line(session, out); +} + +void hybbx_monitor_broadcast(hybbx_service_t *service, const char *line) +{ + mon_bcast_ctx_t ctx; + + if (service == NULL || line == NULL || line[0] == '\0') { + return; + } + + ctx.line = line; + ctx.skip = NULL; + hybbx_service_visit_sessions(service, mon_bcast_visitor, &ctx); +} + +void hybbx_monitor_event(hybbx_service_t *service, + const char *username, + const char *plugin, + const char *detail) +{ + char line[MONITOR_LINE_MAX]; + const char *user = (username != NULL && username[0] != '\0') ? username : "?"; + const char *plug = (plugin != NULL && plugin[0] != '\0') ? plugin : "?"; + + if (service == NULL || !g_any_monitor_active) { + /* Still compute flag lazily */ + monitor_refresh_active_flag(service); + if (!g_any_monitor_active) { + return; + } + } + + if (detail != NULL && detail[0] != '\0') { + snprintf(line, sizeof(line), "%s@%s %s", user, plug, detail); + } else { + snprintf(line, sizeof(line), "%s@%s", user, plug); + } + + hybbx_monitor_broadcast(service, line); +} + +static void monitor_push_raw_line(hybbx_service_t *service, const char *tag, + const char *text) +{ + char line[MONITOR_LINE_MAX]; + + if (text == NULL) { + return; + } + while (*text == '\r' || *text == '\n') { + text++; + } + if (*text == '\0') { + return; + } + + snprintf(line, sizeof(line), "%s %s", tag, text); + hybbx_monitor_broadcast(service, line); +} + +static void monitor_tail_read(hybbx_service_t *service, monitor_tail_t *tail, + const char *tag) +{ + FILE *fp; + struct stat st; + char chunk[MONITOR_READ_CHUNK]; + size_t n; + char *p; + char *nl; + + if (tail == NULL || !tail->active || tail->path[0] == '\0') { + return; + } + + if (stat(tail->path, &st) != 0) { + return; + } + + if (st.st_size < tail->offset) { + /* Truncated / rotated — follow from start of new content. */ + tail->offset = 0; + tail->carry_len = 0; + } + + if (st.st_size == tail->offset) { + return; + } + + fp = fopen(tail->path, "r"); + if (fp == NULL) { + return; + } + + if (fseeko(fp, tail->offset, SEEK_SET) != 0) { + fclose(fp); + return; + } + + while ((n = fread(chunk, 1, sizeof(chunk) - 1, fp)) > 0) { + chunk[n] = '\0'; + p = chunk; + while (*p != '\0') { + nl = strchr(p, '\n'); + if (nl == NULL) { + size_t left = strlen(p); + if (tail->carry_len + left >= sizeof(tail->carry)) { + tail->carry_len = 0; + } + memcpy(tail->carry + tail->carry_len, p, left); + tail->carry_len += left; + tail->carry[tail->carry_len] = '\0'; + break; + } + *nl = '\0'; + if (tail->carry_len > 0) { + size_t room = sizeof(tail->carry) - 1u - tail->carry_len; + size_t take = strlen(p); + if (take > room) { + take = room; + } + memcpy(tail->carry + tail->carry_len, p, take); + tail->carry_len += take; + tail->carry[tail->carry_len] = '\0'; + monitor_push_raw_line(service, tag, tail->carry); + tail->carry_len = 0; + } else { + monitor_push_raw_line(service, tag, p); + } + p = nl + 1; + } + tail->offset = ftello(fp); + } + + fclose(fp); + + /* Refresh path if weekly rotate changed hybbx log name. */ + if (tail == &g_tail_hybbx) { + char path[HYBBX_PATH_MAX]; + if (hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK && + strcmp(path, tail->path) != 0) { + monitor_tail_seek_end(tail, path); + } + } +} + +void hybbx_monitor_tick(hybbx_service_t *service) +{ + if (!g_mon_ready || !g_mon_cfg.enabled || service == NULL) { + return; + } + + monitor_refresh_active_flag(service); + if (!g_any_monitor_active) { + return; + } + + if (!g_tail_hybbx.active && g_mon_cfg.follow_hybbx) { + char path[HYBBX_PATH_MAX]; + if (hybbx_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_hybbx, path); + } + } + if (!g_tail_security.active && g_mon_cfg.follow_security) { + char path[HYBBX_PATH_MAX]; + if (hybbx_security_log_current_path(path, sizeof(path)) == HYBBX_OK) { + monitor_tail_seek_end(&g_tail_security, path); + } + } + + if (g_mon_cfg.follow_hybbx) { + monitor_tail_read(service, &g_tail_hybbx, "hybbx:"); + } + if (g_mon_cfg.follow_security) { + monitor_tail_read(service, &g_tail_security, "security:"); + } +} + +void hybbx_monitor_shutdown(void) +{ + memset(&g_mon_cfg, 0, sizeof(g_mon_cfg)); + memset(&g_tail_hybbx, 0, sizeof(g_tail_hybbx)); + memset(&g_tail_security, 0, sizeof(g_tail_security)); + g_mon_ready = 0; + g_any_monitor_active = 0; +} diff --git a/src/core/networks.c b/src/core/networks.c new file mode 100644 index 0000000..c52554d --- /dev/null +++ b/src/core/networks.c @@ -0,0 +1,191 @@ +#include "hybbx/networks.h" +#include "hybbx/instance.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int networks_has_key(const hybbx_config_t *config, const char *key) +{ + const char *value; + + if (config == NULL || key == NULL) { + return 0; + } + + value = hybbx_config_get(config, "networks", key, NULL); + return value != NULL && value[0] != '\0'; +} + +void hybbx_networks_config_defaults(hybbx_networks_config_t *networks) +{ + if (networks == NULL) { + return; + } + + networks->telnet = 1; + networks->ax25 = 0; + networks->baycom = 0; + networks->ardop = 0; + networks->crdop = 0; + networks->ssh = 0; + networks->websocket = 0; + networks->circuit = 1; + networks->mains_proxy = 0; +} + +void hybbx_networks_config_apply(hybbx_networks_config_t *networks, + const hybbx_config_t *config) +{ + if (networks == NULL) { + return; + } + + hybbx_networks_config_defaults(networks); + + if (config == NULL) { + return; + } + + if (networks_has_key(config, "telnet")) { + networks->telnet = hybbx_config_get_bool(config, "networks", + "telnet", 1); + } + + networks->ax25 = hybbx_config_get_bool(config, "networks", "ax25", 0); + + if (networks_has_key(config, "baycom")) { + networks->baycom = hybbx_config_get_bool(config, "networks", "baycom", 0); + } + + if (networks_has_key(config, "ardop")) { + networks->ardop = hybbx_config_get_bool(config, "networks", "ardop", 0); + } + + if (networks_has_key(config, "crdop")) { + networks->crdop = hybbx_config_get_bool(config, "networks", "crdop", 0); + } + + networks->ssh = hybbx_config_get_bool(config, "networks", "ssh", 0); + + networks->websocket = hybbx_config_get_bool(config, "networks", + "websocket", 0); + + if (networks_has_key(config, "circuit")) { + networks->circuit = hybbx_config_get_bool(config, "networks", + "circuit", 1); + } else { + networks->circuit = hybbx_config_get_bool(config, "circuit", "enabled", + 1); + } + + if (networks_has_key(config, "mains_proxy")) { + networks->mains_proxy = hybbx_config_get_bool(config, "networks", + "mains_proxy", 0); + } + + hybbx_log_info("[networks] telnet=%s ssh=%s ax25=%s baycom=%s ardop=%s " + "crdop=%s websocket=%s circuit=%s mains_proxy=%s", + hybbx_bool_to_string(networks->telnet), + hybbx_bool_to_string(networks->ssh), + hybbx_bool_to_string(networks->ax25), + hybbx_bool_to_string(networks->baycom), + hybbx_bool_to_string(networks->ardop), + hybbx_bool_to_string(networks->crdop), + hybbx_bool_to_string(networks->websocket), + hybbx_bool_to_string(networks->circuit), + hybbx_bool_to_string(networks->mains_proxy)); +} + +int hybbx_networks_is_static_transport(const char *plugin_name) +{ + if (plugin_name == NULL || plugin_name[0] == '\0') { + return 0; + } + + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_MAIN: + return str_ieq(plugin_name, "websocket"); + case HYBBX_INSTANCE_SECONDARY: + return str_ieq(plugin_name, "mains_proxy"); + case HYBBX_INSTANCE_PROXY: + default: + return 0; + } +} + +int hybbx_networks_transport_wanted(const char *plugin_name, + const hybbx_networks_config_t *networks) +{ + if (plugin_name == NULL || plugin_name[0] == '\0') { + return 0; + } + + if (!hybbx_instance_plugin_allowed(plugin_name)) { + return 0; + } + + if (hybbx_networks_is_static_transport(plugin_name)) { + return 1; + } + + if (networks == NULL) { + return 0; + } + + if (str_ieq(plugin_name, "telnet")) { + return networks->telnet; + } + + if (str_ieq(plugin_name, "packet_radio")) { + return networks->ax25; + } + + if (str_ieq(plugin_name, "baycom")) { + return networks->baycom; + } + + if (str_ieq(plugin_name, "ardop")) { + return networks->ardop; + } + + if (str_ieq(plugin_name, "crdop")) { + return networks->crdop; + } + + if (str_ieq(plugin_name, "ssh")) { + return networks->ssh; + } + + if (str_ieq(plugin_name, "websocket")) { + return networks->websocket; + } + + if (str_ieq(plugin_name, "mains_proxy")) { + return networks->mains_proxy; + } + + return 0; +} diff --git a/src/core/password.c b/src/core/password.c new file mode 100644 index 0000000..1e8854a --- /dev/null +++ b/src/core/password.c @@ -0,0 +1,396 @@ +#include "hybbx/password.h" +#include "hybbx/crypto.h" +#include "hybbx/util.h" + +#include "crypto_backends.h" + +#include +#include +#include +#include + +#define MD5_HEX_SIZE (32 + 1) + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int hex_digit(char ch) +{ + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') { + return 10 + (ch - 'a'); + } + if (ch >= 'A' && ch <= 'F') { + return 10 + (ch - 'A'); + } + return -1; +} + +static int hex_string_eq(const char *a, const char *b, size_t hex_len) +{ + size_t i; + + for (i = 0; i < hex_len; i++) { + if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) { + return 0; + } + } + + return 1; +} + +static int hex_string_valid(const char *s, size_t hex_len) +{ + size_t i; + + if (s == NULL) { + return 0; + } + + for (i = 0; i < hex_len; i++) { + if (hex_digit(s[i]) < 0) { + return 0; + } + } + + return s[hex_len] == '\0'; +} + +int hybbx_password_is_hashed(const char *stored) +{ + if (stored == NULL || stored[0] == '\0') { + return 0; + } + + if (strncmp(stored, HYBBX_PASSWORD_SHA256_PREFIX, 8) == 0) { + return hex_string_valid(stored + 8, 64); + } + + if (strncmp(stored, HYBBX_PASSWORD_MD5_PREFIX, 5) == 0) { + return hex_string_valid(stored + 5, 32); + } + + return 0; +} + +int hybbx_password_is_plain(const char *stored) +{ + if (stored == NULL || stored[0] == '\0') { + return 0; + } + + return !hybbx_password_is_hashed(stored); +} + +hybbx_result_t hybbx_password_hash_sha256(const char *plain, + char *out, + size_t out_size) +{ + char hex[HYBBX_BACKEND_SHA256_HEX_SIZE]; + size_t need; + + if (plain == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + need = strlen(HYBBX_PASSWORD_SHA256_PREFIX) + 64 + 1; + if (out_size < need) { + return HYBBX_ERR_INVALID; + } + + hybbx_backend_sha256_hex(plain, strlen(plain), hex); + snprintf(out, out_size, "%s%s", HYBBX_PASSWORD_SHA256_PREFIX, hex); + return HYBBX_OK; +} + +hybbx_result_t hybbx_password_hash(const char *plain, char *out, size_t out_size) +{ + return hybbx_password_hash_sha256(plain, out, out_size); +} + +/* Minimal MD5 (RFC 1321) for legacy {md5} verification only. */ +typedef struct md5_ctx { + uint32_t state[4]; + uint32_t count[2]; + uint8_t buffer[64]; +} md5_ctx_t; + +#define MD5_F(x, y, z) (((x) & (y)) | ((~x) & (z))) +#define MD5_G(x, y, z) (((x) & (z)) | ((y) & (~z))) +#define MD5_H(x, y, z) ((x) ^ (y) ^ (z)) +#define MD5_I(x, y, z) ((y) ^ ((x) | (~z))) +#define MD5_ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n)))) + +#define MD5_FF(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_F((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +#define MD5_GG(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_G((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +#define MD5_HH(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_H((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +#define MD5_II(a, b, c, d, x, s, ac) \ + do { \ + (a) += MD5_I((b), (c), (d)) + (x) + (uint32_t)(ac); \ + (a) = MD5_ROTATE_LEFT((a), (s)); \ + (a) += (b); \ + } while (0) + +static void md5_encode(uint8_t *out, const uint32_t *in, size_t len) +{ + size_t i; + size_t j; + + for (i = 0, j = 0; j < len; i++, j += 4) { + out[j] = (uint8_t)(in[i] & 0xff); + out[j + 1] = (uint8_t)((in[i] >> 8) & 0xff); + out[j + 2] = (uint8_t)((in[i] >> 16) & 0xff); + out[j + 3] = (uint8_t)((in[i] >> 24) & 0xff); + } +} + +static void md5_transform(uint32_t state[4], const uint8_t block[64]) +{ + uint32_t a = state[0]; + uint32_t b = state[1]; + uint32_t c = state[2]; + uint32_t d = state[3]; + uint32_t x[16]; + size_t i; + + for (i = 0; i < 16; i++) { + x[i] = ((uint32_t)block[i * 4]) | + ((uint32_t)block[i * 4 + 1] << 8) | + ((uint32_t)block[i * 4 + 2] << 16) | + ((uint32_t)block[i * 4 + 3] << 24); + } + + MD5_FF(a, b, c, d, x[0], 7, 0xd76aa478); + MD5_FF(d, a, b, c, x[1], 12, 0xe8c7b756); + MD5_FF(c, d, a, b, x[2], 17, 0x242070db); + MD5_FF(b, c, d, a, x[3], 22, 0xc1bdceee); + MD5_FF(a, b, c, d, x[4], 7, 0xf57c0faf); + MD5_FF(d, a, b, c, x[5], 12, 0x4787c62a); + MD5_FF(c, d, a, b, x[6], 17, 0xa8304613); + MD5_FF(b, c, d, a, x[7], 22, 0xfd469501); + MD5_FF(a, b, c, d, x[8], 7, 0x698098d8); + MD5_FF(d, a, b, c, x[9], 12, 0x8b44f7af); + MD5_FF(c, d, a, b, x[10], 17, 0xffff5bb1); + MD5_FF(b, c, d, a, x[11], 22, 0x895cd7be); + MD5_FF(a, b, c, d, x[12], 7, 0x6b901122); + MD5_FF(d, a, b, c, x[13], 12, 0xfd987193); + MD5_FF(c, d, a, b, x[14], 17, 0xa679438e); + MD5_FF(b, c, d, a, x[15], 22, 0x49b40821); + MD5_GG(a, b, c, d, x[1], 5, 0xf61e2562); + MD5_GG(d, a, b, c, x[6], 9, 0xc040b340); + MD5_GG(c, d, a, b, x[11], 14, 0x265e5a51); + MD5_GG(b, c, d, a, x[0], 20, 0xe9b6c7aa); + MD5_GG(a, b, c, d, x[5], 5, 0xd62f105d); + MD5_GG(d, a, b, c, x[10], 9, 0x02441453); + MD5_GG(c, d, a, b, x[15], 14, 0xd8a1e681); + MD5_GG(b, c, d, a, x[4], 20, 0xe7d3fbc8); + MD5_GG(a, b, c, d, x[9], 5, 0x21e1cde6); + MD5_GG(d, a, b, c, x[14], 9, 0xc33707d6); + MD5_GG(c, d, a, b, x[3], 14, 0xf4d50d87); + MD5_GG(b, c, d, a, x[8], 20, 0x455a14ed); + MD5_GG(a, b, c, d, x[13], 5, 0xa9e3e905); + MD5_GG(d, a, b, c, x[2], 9, 0xfcefa3f8); + MD5_GG(c, d, a, b, x[7], 14, 0x676f02d9); + MD5_GG(b, c, d, a, x[12], 20, 0x8d2a4c8a); + MD5_HH(a, b, c, d, x[5], 4, 0xfffa3942); + MD5_HH(d, a, b, c, x[8], 11, 0x8771f681); + MD5_HH(c, d, a, b, x[11], 16, 0x6d9d6122); + MD5_HH(b, c, d, a, x[14], 23, 0xfde5380c); + MD5_HH(a, b, c, d, x[1], 4, 0xa4beea44); + MD5_HH(d, a, b, c, x[4], 11, 0x4bdecfa9); + MD5_HH(c, d, a, b, x[7], 16, 0xf6bb4b60); + MD5_HH(b, c, d, a, x[10], 23, 0xbebfbc70); + MD5_HH(a, b, c, d, x[13], 4, 0x289b7ec6); + MD5_HH(d, a, b, c, x[0], 11, 0xeaa127fa); + MD5_HH(c, d, a, b, x[3], 16, 0xd4ef3085); + MD5_HH(b, c, d, a, x[6], 23, 0x04881d05); + MD5_HH(a, b, c, d, x[9], 4, 0xd9d4d039); + MD5_HH(d, a, b, c, x[12], 11, 0xe6db99e5); + MD5_HH(c, d, a, b, x[15], 16, 0x1fa27cf8); + MD5_HH(b, c, d, a, x[2], 23, 0xc4ac5665); + MD5_II(a, b, c, d, x[0], 6, 0xf4292244); + MD5_II(d, a, b, c, x[7], 10, 0x432aff97); + MD5_II(c, d, a, b, x[14], 15, 0xab9423a7); + MD5_II(b, c, d, a, x[5], 21, 0xfc93a039); + MD5_II(a, b, c, d, x[12], 6, 0x655b59c3); + MD5_II(d, a, b, c, x[3], 10, 0x8f0ccc92); + MD5_II(c, d, a, b, x[10], 15, 0xffeff47d); + MD5_II(b, c, d, a, x[1], 21, 0x85845dd1); + MD5_II(a, b, c, d, x[8], 6, 0x6fa87e4f); + MD5_II(d, a, b, c, x[15], 10, 0xfe2ce6e0); + MD5_II(c, d, a, b, x[6], 15, 0xa3014314); + MD5_II(b, c, d, a, x[13], 21, 0x4e0811a1); + MD5_II(a, b, c, d, x[4], 6, 0xf7537e82); + MD5_II(d, a, b, c, x[11], 10, 0xbd3af235); + MD5_II(c, d, a, b, x[2], 15, 0x2ad7d2bb); + MD5_II(b, c, d, a, x[9], 21, 0xeb86d391); + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; +} + +static void md5_init(md5_ctx_t *ctx) +{ + ctx->count[0] = 0; + ctx->count[1] = 0; + ctx->state[0] = 0x67452301; + ctx->state[1] = 0xefcdab89; + ctx->state[2] = 0x98badcfe; + ctx->state[3] = 0x10325476; +} + +static void md5_update(md5_ctx_t *ctx, const uint8_t *data, size_t len) +{ + size_t i; + size_t index = (size_t)((ctx->count[0] >> 3) & 0x3f); + + if ((ctx->count[0] += (uint32_t)(len << 3)) < (uint32_t)(len << 3)) { + ctx->count[1]++; + } + ctx->count[1] += (uint32_t)(len >> 29); + + size_t part_len = 64 - index; + + if (len >= part_len) { + memcpy(&ctx->buffer[index], data, part_len); + md5_transform(ctx->state, ctx->buffer); + for (i = part_len; i + 63 < len; i += 64) { + memcpy(ctx->buffer, &data[i], 64); + md5_transform(ctx->state, ctx->buffer); + } + index = 0; + } else { + i = 0; + } + + memcpy(&ctx->buffer[index], &data[i], len - i); +} + +static void md5_finalize_hex(md5_ctx_t *ctx, char *hex) +{ + uint8_t bits[8]; + uint8_t digest[16]; + size_t index = (size_t)((ctx->count[0] >> 3) & 0x3f); + size_t pad_len = (index < 56) ? (56 - index) : (120 - index); + static const uint8_t padding[64] = { 0x80 }; + size_t i; + + md5_encode(bits, ctx->count, 8); + md5_update(ctx, padding, pad_len); + md5_update(ctx, bits, 8); + md5_encode(digest, ctx->state, 16); + + for (i = 0; i < 16; i++) { + snprintf(hex + i * 2, 3, "%02x", digest[i]); + } +} + +static void md5_hex(const char *plain, char *hex) +{ + md5_ctx_t ctx; + + md5_init(&ctx); + md5_update(&ctx, (const uint8_t *)plain, strlen(plain)); + md5_finalize_hex(&ctx, hex); +} + +int hybbx_password_match(const char *stored, const char *provided) +{ + char hex[HYBBX_BACKEND_SHA256_HEX_SIZE]; + char md5[MD5_HEX_SIZE]; + + if (provided == NULL || provided[0] == '\0') { + return 0; + } + + if (stored == NULL || stored[0] == '\0') { + return 0; + } + + if (strncmp(stored, HYBBX_PASSWORD_SHA256_PREFIX, 8) == 0) { + hybbx_backend_sha256_hex(provided, strlen(provided), hex); + return hex_string_eq(stored + 8, hex, 64); + } + + if (strncmp(stored, HYBBX_PASSWORD_MD5_PREFIX, 5) == 0) { + md5_hex(provided, md5); + return hex_string_eq(stored + 5, md5, 32); + } + + return str_ieq(stored, provided); +} + +hybbx_result_t hybbx_password_generate_alnum(char *out, + size_t out_size, + size_t min_len, + size_t max_len) +{ + static const char alphabet[] = "abcdefghijklmnopqrstuvwxyz0123456789"; + uint8_t rnd[32]; + size_t len; + size_t span; + size_t i; + size_t rnd_off = sizeof(rnd); + + if (out == NULL || out_size == 0 || min_len == 0 || min_len > max_len || + max_len >= out_size) { + return HYBBX_ERR_INVALID; + } + + span = max_len - min_len + 1u; + if (hybbx_crypto_random(rnd, sizeof(rnd)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + len = min_len + (size_t)(rnd[0] % (unsigned)span); + for (i = 0; i < len; i++) { + if (rnd_off >= sizeof(rnd)) { + if (hybbx_crypto_random(rnd, sizeof(rnd)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + rnd_off = 0; + } + out[i] = alphabet[rnd[rnd_off++] % 36u]; + } + out[len] = '\0'; + return HYBBX_OK; +} diff --git a/src/core/privilege.c b/src/core/privilege.c new file mode 100644 index 0000000..254dbc6 --- /dev/null +++ b/src/core/privilege.c @@ -0,0 +1,233 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/privilege.h" +#include "hybbx/config.h" +#include "hybbx/log.h" +#include "hybbx/types.h" + +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#include +#include +#include +#endif + +#if !defined(_WIN32) + +static int parse_optional_uid(const char *raw, uid_t *out) +{ + char *end; + unsigned long value; + + if (raw == NULL || raw[0] == '\0') { + return 0; + } + errno = 0; + value = strtoul(raw, &end, 10); + if (end == raw || *end != '\0' || errno != 0) { + return -1; + } + *out = (uid_t)value; + return 1; +} + +static int parse_optional_gid(const char *raw, gid_t *out) +{ + char *end; + unsigned long value; + + if (raw == NULL || raw[0] == '\0') { + return 0; + } + errno = 0; + value = strtoul(raw, &end, 10); + if (end == raw || *end != '\0' || errno != 0) { + return -1; + } + *out = (gid_t)value; + return 1; +} + +static hybbx_result_t resolve_target( + const hybbx_config_t *config, + uid_t *uid_out, + gid_t *gid_out, + char name_buf[64], + size_t name_buf_len) +{ + const char *user_name; + const char *group_name; + const char *uid_raw; + const char *gid_raw; + struct passwd *pw = NULL; + struct group *gr = NULL; + uid_t uid_override = 0; + gid_t gid_override = 0; + int have_uid = 0; + int have_gid = 0; + + user_name = hybbx_config_get(config, "service", "user", NULL); + group_name = hybbx_config_get(config, "service", "group", NULL); + uid_raw = hybbx_config_get(config, "service", "uid", NULL); + gid_raw = hybbx_config_get(config, "service", "gid", NULL); + + have_uid = parse_optional_uid(uid_raw, &uid_override); + if (have_uid < 0) { + hybbx_log_warn("[service] invalid uid=%s", uid_raw); + return HYBBX_ERR_INVALID; + } + have_gid = parse_optional_gid(gid_raw, &gid_override); + if (have_gid < 0) { + hybbx_log_warn("[service] invalid gid=%s", gid_raw); + return HYBBX_ERR_INVALID; + } + + if (user_name != NULL && user_name[0] != '\0') { + pw = getpwnam(user_name); + if (pw == NULL) { + hybbx_log_warn("[service] unknown user=%s", user_name); + return HYBBX_ERR_INVALID; + } + *uid_out = pw->pw_uid; + *gid_out = pw->pw_gid; + if (name_buf_len > 0) { + snprintf(name_buf, name_buf_len, "%s", pw->pw_name); + } + } else if (have_uid) { + pw = getpwuid(uid_override); + if (pw == NULL) { + hybbx_log_warn("[service] unknown uid=%s", uid_raw); + return HYBBX_ERR_INVALID; + } + *uid_out = pw->pw_uid; + *gid_out = pw->pw_gid; + if (name_buf_len > 0) { + snprintf(name_buf, name_buf_len, "%s", pw->pw_name); + } + } else { + return HYBBX_ERR_INVALID; + } + + if (have_uid) { + *uid_out = uid_override; + } + if (group_name != NULL && group_name[0] != '\0') { + gr = getgrnam(group_name); + if (gr == NULL) { + hybbx_log_warn("[service] unknown group=%s", group_name); + return HYBBX_ERR_INVALID; + } + *gid_out = gr->gr_gid; + } else if (have_gid) { + if (getgrgid(gid_override) == NULL) { + hybbx_log_warn("[service] unknown gid=%s", gid_raw); + return HYBBX_ERR_INVALID; + } + *gid_out = gid_override; + } + + return HYBBX_OK; +} + +static hybbx_result_t drop_to(uid_t uid, gid_t gid, const char *name) +{ + if (getuid() != 0) { + hybbx_log_warn("[service] privilege drop requires root start (euid=0)"); + return HYBBX_ERR_DENIED; + } + + if (name != NULL && name[0] != '\0') { + if (initgroups(name, gid) != 0) { + hybbx_log_warn("[service] initgroups(%s) failed: %s", + name, strerror(errno)); + return HYBBX_ERR_IO; + } + } + + if (setgid(gid) != 0) { + hybbx_log_warn("[service] setgid(%u) failed: %s", + (unsigned)gid, strerror(errno)); + return HYBBX_ERR_IO; + } + + if (setuid(uid) != 0) { + hybbx_log_warn("[service] setuid(%u) failed: %s", + (unsigned)uid, strerror(errno)); + return HYBBX_ERR_IO; + } + + if (setuid(0) == 0 || seteuid(0) == 0) { + hybbx_log_warn("[service] privilege drop incomplete — still root"); + return HYBBX_ERR_IO; + } + + hybbx_log_info("[service] dropped privileges to uid=%u gid=%u (%s)", + (unsigned)uid, (unsigned)gid, + name != NULL && name[0] != '\0' ? name : "?"); + return HYBBX_OK; +} + +#endif /* !_WIN32 */ + +hybbx_result_t hybbx_privilege_apply_from_config(const hybbx_config_t *config) +{ +#if defined(_WIN32) + (void)config; + return HYBBX_OK; +#else + const char *user_name; + uid_t target_uid = 0; + gid_t target_gid = 0; + char resolved_name[64]; + hybbx_result_t rc; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + resolved_name[0] = '\0'; + user_name = hybbx_config_get(config, "service", "user", NULL); + + if (geteuid() == 0) { + if ((user_name == NULL || user_name[0] == '\0') && + hybbx_config_get(config, "service", "uid", NULL) == NULL) { + hybbx_log_warn( + "[service] refusing to run as root — set [service] user= " + "(and optional group=)"); + return HYBBX_ERR_DENIED; + } + + rc = resolve_target(config, &target_uid, &target_gid, + resolved_name, sizeof(resolved_name)); + if (rc != HYBBX_OK) { + return rc; + } + + return drop_to(target_uid, target_gid, + resolved_name[0] != '\0' ? resolved_name : user_name); + } + + if (user_name != NULL && user_name[0] != '\0') { + struct passwd *pw = getpwnam(user_name); + + if (pw == NULL) { + hybbx_log_warn("[service] unknown configured user=%s", user_name); + return HYBBX_OK; + } + if ((uid_t)geteuid() != pw->pw_uid) { + hybbx_log_warn( + "[service] running as uid=%u but [service] user=%s (uid=%u)", + (unsigned)geteuid(), user_name, (unsigned)pw->pw_uid); + } + } + + return HYBBX_OK; +#endif +} diff --git a/src/core/proxychat.c b/src/core/proxychat.c new file mode 100644 index 0000000..d26a178 --- /dev/null +++ b/src/core/proxychat.c @@ -0,0 +1,134 @@ +#include "hybbx/proxychat.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/service.h" +#include "hybbx/session.h" + +#include +#include + +typedef struct proxychat_fanout_ctx { + hybbx_session_t *from; + const char *line; + const char *from_address; +} proxychat_fanout_ctx_t; + +static void proxychat_local_visitor(hybbx_session_t *session, void *userdata) +{ + proxychat_fanout_ctx_t *ctx = (proxychat_fanout_ctx_t *)userdata; + char line[HYBBX_PROXYMAIL_ADDRESS_MAX + HYBBX_LINE_MAX + 8]; + + if (session == NULL || ctx == NULL || ctx->line == NULL) { + return; + } + + if (session == ctx->from) { + return; + } + + if (hybbx_session_area(session) != HYBBX_AREA_PROXYCHAT) { + return; + } + + if (hybbx_session_is_guest(session)) { + return; + } + + snprintf(line, sizeof(line), "<%s> %s", + ctx->from_address != NULL ? ctx->from_address : "?", + ctx->line); + hybbx_session_write_line(session, line); +} + +static void proxychat_show_local(hybbx_session_t *session, + const char *from_address, + const char *line) +{ + char out[HYBBX_PROXYMAIL_ADDRESS_MAX + HYBBX_LINE_MAX + 8]; + + snprintf(out, sizeof(out), "<%s> %s", + from_address != NULL ? from_address : "?", line); + hybbx_session_write_line(session, out); +} + +hybbx_result_t hybbx_proxychat_post(hybbx_service_t *service, + hybbx_session_t *session, + const char *line) +{ + char from_address[HYBBX_PROXYMAIL_ADDRESS_MAX]; + proxychat_fanout_ctx_t ctx; + hybbx_result_t rc; + + if (session == NULL || line == NULL || line[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (service == NULL) { + return HYBBX_ERR_INVALID; + } + + snprintf(from_address, sizeof(from_address), "%s@%s", + hybbx_session_display_name(session), + hybbx_service_get_name(service)); + + proxychat_show_local(session, from_address, line); + + if (!hybbx_mains_proxy_mesh_active()) { + hybbx_session_write_line(session, + "Mains proxy is not running — message shown locally only."); + return HYBBX_OK; + } + + ctx.from = session; + ctx.line = line; + ctx.from_address = from_address; + hybbx_service_visit_sessions(service, proxychat_local_visitor, &ctx); + + rc = hybbx_mains_proxy_send_chat(service, from_address, line); + if (rc == HYBBX_ERR_UNSUPPORTED) { + hybbx_session_write_line(session, + "No outbound peer links — message shown locally only."); + return HYBBX_OK; + } + if (rc == HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(session, + "No peer links available to relay your message."); + return HYBBX_OK; + } + + return rc; +} + +void hybbx_proxychat_receive(hybbx_service_t *service, + const char *from_address, + const char *line) +{ + proxychat_fanout_ctx_t ctx; + + if (service == NULL || line == NULL || line[0] == '\0') { + return; + } + + ctx.from = NULL; + ctx.line = line; + ctx.from_address = from_address; + hybbx_service_visit_sessions(service, proxychat_local_visitor, &ctx); +} + +void hybbx_proxychat_show_banner(hybbx_session_t *session) +{ + if (session == NULL) { + return; + } + + hybbx_session_write_line(session, + "Proxychat — talk with users on linked mains."); + if (hybbx_mains_proxy_mesh_active()) { + hybbx_session_write_line(session, + "Type a line to send; /leave or /main to exit."); + } else { + hybbx_session_write_line(session, + "No peer links active — check mains_proxy configuration."); + hybbx_session_write_line(session, + "Type a line to try; /leave or /main to exit."); + } +} diff --git a/src/core/proxymail.c b/src/core/proxymail.c new file mode 100644 index 0000000..3981dde --- /dev/null +++ b/src/core/proxymail.c @@ -0,0 +1,676 @@ +#include "hybbx/proxymail.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" + +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_PROXYMAIL_DIR_NAME "proxymail" +#define HYBBX_PROXYMAIL_INBOX_NAME "inbox" +#define HYBBX_PROXYMAIL_RECYCLE_NAME "recycle" +#define HYBBX_PROXYMAIL_NEXT_FILE "proxymail.next" + +typedef struct proxymail_entry { + uint64_t id; + char from[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + time_t received_at; + int read; +} proxymail_entry_t; + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t proxymail_root_path(hybbx_service_t *service, + char *out, size_t out_len) +{ + hybbx_storage_t *storage; + const char *root; + + storage = hybbx_service_get_storage(service); + root = hybbx_storage_root_path(storage); + if (root == NULL || root[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, root, HYBBX_PROXYMAIL_DIR_NAME); +} + +static hybbx_result_t proxymail_user_inbox_path(hybbx_service_t *service, + const char *username, + char *out, size_t out_len) +{ + char root[HYBBX_PATH_MAX]; + char user_dir[HYBBX_PATH_MAX]; + char user_norm[HYBBX_USER_NAME_MAX]; + + if (username == NULL || username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (proxymail_root_path(service, root, sizeof(root)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(user_norm, username, sizeof(user_norm)); + hybbx_username_normalize(user_norm); + + if (hybbx_path_join(user_dir, sizeof(user_dir), root, user_norm) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, user_dir, HYBBX_PROXYMAIL_INBOX_NAME); +} + +static int parse_msg_filename(const char *name, uint64_t *id) +{ + char *end; + + if (name == NULL || id == NULL) { + return 0; + } + + if (strlen(name) < 5 || strcmp(name + strlen(name) - 4, ".msg") != 0) { + return 0; + } + + *id = (uint64_t)strtoull(name, &end, 10); + return end != name && strcmp(end, ".msg") == 0; +} + +static int proxymail_entry_cmp(const void *a, const void *b) +{ + const proxymail_entry_t *ea = (const proxymail_entry_t *)a; + const proxymail_entry_t *eb = (const proxymail_entry_t *)b; + + if (ea->id < eb->id) { + return 1; + } + if (ea->id > eb->id) { + return -1; + } + return 0; +} + +static hybbx_result_t proxymail_load_inbox(hybbx_service_t *service, + const char *username, + proxymail_entry_t *entries, + size_t max_entries, + size_t *out_count) +{ + char inbox_path[HYBBX_PATH_MAX]; + DIR *dir; + struct dirent *ent; + size_t count = 0; + + if (proxymail_user_inbox_path(service, username, inbox_path, + sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + *out_count = 0; + + dir = opendir(inbox_path); + if (dir == NULL) { + return errno == ENOENT ? HYBBX_OK : HYBBX_ERR_IO; + } + + while ((ent = readdir(dir)) != NULL) { + uint64_t id; + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + proxymail_entry_t entry; + + if (!parse_msg_filename(ent->d_name, &id)) { + continue; + } + + if (count >= max_entries) { + break; + } + + memset(&entry, 0, sizeof(entry)); + entry.id = id; + + if (hybbx_path_join(path, sizeof(path), inbox_path, ent->d_name) != + HYBBX_OK) { + continue; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + continue; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + char *eq = strchr(line, '='); + char *key; + char *value; + + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + break; + } + + if (eq == NULL) { + continue; + } + + *eq = '\0'; + key = line; + value = eq + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + + { + size_t vlen = strlen(value); + + while (vlen > 0 && (value[vlen - 1] == '\n' || + value[vlen - 1] == '\r')) { + value[--vlen] = '\0'; + } + } + + if (str_ieq(key, "from")) { + hybbx_strlcpy(entry.from, value, sizeof(entry.from)); + } else if (str_ieq(key, "subject")) { + hybbx_strlcpy(entry.subject, value, sizeof(entry.subject)); + } else if (str_ieq(key, "time")) { + entry.received_at = (time_t)strtol(value, NULL, 10); + } else if (str_ieq(key, "read")) { + entry.read = hybbx_bool_is_true(value); + } + } + + fclose(fp); + entries[count++] = entry; + } + + closedir(dir); + qsort(entries, count, sizeof(entries[0]), proxymail_entry_cmp); + *out_count = count; + return HYBBX_OK; +} + +static hybbx_result_t proxymail_next_id(hybbx_service_t *service, + uint64_t *out_id) +{ + char root[HYBBX_PATH_MAX]; + char counter_path[HYBBX_PATH_MAX]; + FILE *fp; + unsigned long long n = 0; + + if (proxymail_root_path(service, root, sizeof(root)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + if (mkdir_p(root) != 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(counter_path, sizeof(counter_path), root, + HYBBX_PROXYMAIL_NEXT_FILE) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(counter_path, "r"); + if (fp != NULL) { + if (fscanf(fp, "%llu", &n) != 1) { + n = 0; + } + fclose(fp); + } + + n++; + *out_id = (uint64_t)n; + + fp = fopen(counter_path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", n); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t proxymail_store_message(hybbx_service_t *service, + const char *username, + const char *from_address, + const char *subject, + const char *body) +{ + char inbox_path[HYBBX_PATH_MAX]; + char msg_path[HYBBX_PATH_MAX]; + char msg_name[32]; + uint64_t id; + FILE *fp; + hybbx_result_t rc; + + rc = proxymail_next_id(service, &id); + if (rc != HYBBX_OK) { + return rc; + } + + if (proxymail_user_inbox_path(service, username, inbox_path, + sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + if (mkdir_p(inbox_path) != 0) { + return HYBBX_ERR_IO; + } + + snprintf(msg_name, sizeof(msg_name), "%06llu.msg", + (unsigned long long)id); + if (hybbx_path_join(msg_path, sizeof(msg_path), inbox_path, msg_name) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(msg_path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "from=%s\n", from_address); + fprintf(fp, "subject=%s\n", subject != NULL ? subject : ""); + fprintf(fp, "time=%ld\n", (long)time(NULL)); + fprintf(fp, "read=no\n"); + fprintf(fp, "---\n%s\n", body != NULL ? body : ""); + fclose(fp); + return HYBBX_OK; +} + +int hybbx_proxymail_parse_address(const char *address, + char *user, size_t user_len, + char *remote_service, size_t remote_len) +{ + const char *at; + size_t ulen; + size_t slen; + + if (user != NULL && user_len > 0) { + user[0] = '\0'; + } + if (remote_service != NULL && remote_len > 0) { + remote_service[0] = '\0'; + } + + if (address == NULL || address[0] == '\0') { + return 0; + } + + at = strchr(address, '@'); + if (at == NULL || at == address || at[1] == '\0') { + return 0; + } + + ulen = (size_t)(at - address); + slen = strlen(at + 1); + if (ulen == 0 || slen == 0 || ulen >= HYBBX_USER_NAME_MAX || + slen >= HYBBX_PROXYMAIL_SERVICE_NAME_MAX) { + return 0; + } + + if (user != NULL && user_len > 0) { + memcpy(user, address, ulen); + user[ulen] = '\0'; + hybbx_username_normalize(user); + if (user[0] == '\0') { + return 0; + } + } + + if (remote_service != NULL && remote_len > 0) { + hybbx_strlcpy(remote_service, at + 1, remote_len); + } + + return 1; +} + +static void proxymail_list_print(hybbx_session_t *session, + const proxymail_entry_t *entries, + size_t count, + unsigned from, + unsigned to) +{ + size_t i; + size_t start; + size_t end; + char line[HYBBX_MAIL_SUBJECT_MAX + 96]; + char header[48]; + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (count == 0) { + hybbx_session_write_line(session, "Inbox empty."); + hybbx_session_write_line(session, + "Try: /proxymail send user@mainname Your subject"); + return; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "No messages in that range."); + return; + } + + start = (size_t)(from - 1); + end = (size_t)to; + + if (from == 1 && to == (unsigned)count) { + hybbx_session_write_line(session, "Proxymail inbox (newest first):"); + } else { + snprintf(header, sizeof(header), "Proxymail %u-%u (newest first):", + from, to); + hybbx_session_write_line(session, header); + } + + for (i = start; i < end; i++) { + snprintf(line, sizeof(line), " %zu %s%s %s", + i + 1, + entries[i].read ? " " : "* ", + entries[i].from, + entries[i].subject[0] != '\0' ? entries[i].subject + : "(no subject)"); + hybbx_session_write_line(session, line); + } + + hybbx_session_write_line(session, + " /proxymail read delete list "); +} + +void hybbx_proxymail_list_inbox(hybbx_service_t *service, + hybbx_session_t *session) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + + if (!hybbx_mains_proxy_mesh_active()) { + hybbx_session_write_line(session, + "Proxymail — send mail to user@another-main."); + hybbx_session_write_line(session, + "No peer links active — check mains_proxy configuration."); + } + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read proxymail inbox."); + return; + } + + proxymail_list_print(session, entries, count, 1, (unsigned)count); +} + +void hybbx_proxymail_list_inbox_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, unsigned to) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read proxymail inbox."); + return; + } + + proxymail_list_print(session, entries, count, from, to); +} + +hybbx_result_t hybbx_proxymail_read(hybbx_service_t *service, + hybbx_session_t *session, + unsigned list_index) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + const proxymail_entry_t *entry; + char inbox_path[HYBBX_PATH_MAX]; + char msg_path[HYBBX_PATH_MAX]; + char msg_name[32]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + int in_body = 0; + + if (list_index == 0) { + return HYBBX_ERR_INVALID; + } + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + hybbx_session_write_line(session, "Could not read proxymail inbox."); + return HYBBX_ERR_IO; + } + + if ((size_t)list_index > count) { + hybbx_session_write_line(session, "No message at that index."); + return HYBBX_ERR_NOT_FOUND; + } + + entry = &entries[list_index - 1]; + + if (proxymail_user_inbox_path(service, hybbx_session_username(session), + inbox_path, sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + snprintf(msg_name, sizeof(msg_name), "%06llu.msg", + (unsigned long long)entry->id); + if (hybbx_path_join(msg_path, sizeof(msg_path), inbox_path, msg_name) != + HYBBX_OK) { + return HYBBX_ERR_IO; + } + + fp = fopen(msg_path, "r"); + if (fp == NULL) { + hybbx_session_write_line(session, "Could not open message."); + return HYBBX_ERR_IO; + } + + hybbx_session_write_line(session, entry->subject[0] != '\0' + ? entry->subject : "(no subject)"); + hybbx_session_write_line(session, entry->from); + + while (fgets(line, sizeof(line), fp) != NULL) { + if (!in_body) { + if (line[0] == '-' && line[1] == '-' && line[2] == '-') { + in_body = 1; + } + continue; + } + + while (line[0] != '\0' && + (line[strlen(line) - 1] == '\n' || + line[strlen(line) - 1] == '\r')) { + line[strlen(line) - 1] = '\0'; + } + + hybbx_session_write_line(session, line); + } + + fclose(fp); + return HYBBX_OK; +} + +hybbx_result_t hybbx_proxymail_delete_range(hybbx_service_t *service, + hybbx_session_t *session, + unsigned from, unsigned to) +{ + proxymail_entry_t entries[HYBBX_MAIL_MAX_MESSAGES]; + size_t count = 0; + char inbox_path[HYBBX_PATH_MAX]; + size_t i; + unsigned removed = 0; + + if (proxymail_load_inbox(service, hybbx_session_username(session), + entries, HYBBX_MAIL_MAX_MESSAGES, + &count) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (to == 0 || to > count) { + to = (unsigned)count; + } + + if (from == 0 || from > count || from > to) { + hybbx_session_write_line(session, "Nothing to delete."); + return HYBBX_OK; + } + + if (proxymail_user_inbox_path(service, hybbx_session_username(session), + inbox_path, sizeof(inbox_path)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + for (i = (size_t)(from - 1); i < (size_t)to; i++) { + char msg_path[HYBBX_PATH_MAX]; + char msg_name[32]; + + snprintf(msg_name, sizeof(msg_name), "%06llu.msg", + (unsigned long long)entries[i].id); + if (hybbx_path_join(msg_path, sizeof(msg_path), inbox_path, + msg_name) == HYBBX_OK) { + if (remove(msg_path) == 0) { + removed++; + } + } + } + + if (removed == 0) { + hybbx_session_write_line(session, "Nothing to delete."); + } else { + char buf[48]; + + snprintf(buf, sizeof(buf), "Deleted %u proxymail message(s).", removed); + hybbx_session_write_line(session, buf); + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_proxymail_recycle_empty(hybbx_service_t *service, + hybbx_session_t *session) +{ + (void)service; + hybbx_session_write_line(session, "Recycle bin empty."); + return HYBBX_OK; +} + +hybbx_result_t hybbx_proxymail_deliver(hybbx_service_t *service, + const char *from_user, + const char *to_address, + const char *subject, + const char *body) +{ + char from_address[HYBBX_PROXYMAIL_ADDRESS_MAX]; + const char *local_name; + + if (!hybbx_proxymail_parse_address(to_address, NULL, 0, NULL, 0)) { + return HYBBX_ERR_INVALID; + } + + local_name = hybbx_service_get_name(service); + snprintf(from_address, sizeof(from_address), "%s@%s", + from_user != NULL ? from_user : "user", local_name); + + return hybbx_mains_proxy_send_mail(service, from_address, to_address, + subject, body); +} + +void hybbx_proxymail_receive(hybbx_service_t *service, + const char *from_address, + const char *to_address, + const char *subject, + const char *body) +{ + char user[HYBBX_USER_NAME_MAX]; + char remote[HYBBX_PROXYMAIL_SERVICE_NAME_MAX]; + const char *local_name; + + if (service == NULL || to_address == NULL) { + return; + } + + if (!hybbx_proxymail_parse_address(to_address, user, sizeof(user), + remote, sizeof(remote))) { + return; + } + + local_name = hybbx_service_get_name(service); + if (!str_ieq(remote, local_name)) { + return; + } + + (void)proxymail_store_message(service, user, + from_address != NULL ? from_address + : "unknown", + subject, body); +} diff --git a/src/core/registry.c b/src/core/registry.c new file mode 100644 index 0000000..107cc66 --- /dev/null +++ b/src/core/registry.c @@ -0,0 +1,83 @@ +#include "hybbx/registry.h" +#include "hybbx/limits.h" + +#include + +typedef struct registry_entry { + const hybbx_transport_plugin_t *plugin; +} registry_entry_t; + +static registry_entry_t g_registry[HYBBX_MAX_PLUGINS]; +static size_t g_registry_count = 0; + +hybbx_result_t hybbx_registry_register(const hybbx_transport_plugin_t *plugin) +{ + if (plugin == NULL || plugin->name == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_registry_find(plugin->name) != NULL) { + return HYBBX_ERR_BUSY; + } + + if (g_registry_count >= HYBBX_MAX_PLUGINS) { + return HYBBX_ERR_NOMEM; + } + + g_registry[g_registry_count].plugin = plugin; + g_registry_count++; + return HYBBX_OK; +} + +hybbx_result_t hybbx_registry_unregister(const char *name) +{ + size_t i; + + if (name == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < g_registry_count; i++) { + if (strcmp(g_registry[i].plugin->name, name) == 0) { + size_t remaining = g_registry_count - i - 1; + if (remaining > 0) { + memmove(&g_registry[i], &g_registry[i + 1], + remaining * sizeof(registry_entry_t)); + } + g_registry_count--; + return HYBBX_OK; + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +const hybbx_transport_plugin_t *hybbx_registry_find(const char *name) +{ + size_t i; + + if (name == NULL) { + return NULL; + } + + for (i = 0; i < g_registry_count; i++) { + if (strcmp(g_registry[i].plugin->name, name) == 0) { + return g_registry[i].plugin; + } + } + + return NULL; +} + +void hybbx_registry_foreach(hybbx_registry_iter_fn fn, void *userdata) +{ + size_t i; + + if (fn == NULL) { + return; + } + + for (i = 0; i < g_registry_count; i++) { + fn(g_registry[i].plugin, userdata); + } +} diff --git a/src/core/rf_tx_pace.c b/src/core/rf_tx_pace.c new file mode 100644 index 0000000..ec88a0e --- /dev/null +++ b/src/core/rf_tx_pace.c @@ -0,0 +1,100 @@ +#include "hybbx/rf_tx_pace.h" +#include "hybbx/posix_time.h" + +#include + +static pthread_mutex_t g_pace_lock = PTHREAD_MUTEX_INITIALIZER; +static struct timespec g_last_tx; +static struct timespec g_burst_start; +static int g_have_last_tx; + +static void timespec_now(struct timespec *ts) +{ + clock_gettime(CLOCK_MONOTONIC, ts); +} + +static long timespec_diff_ms(const struct timespec *later, + const struct timespec *earlier) +{ + return (later->tv_sec - earlier->tv_sec) * 1000L + + (later->tv_nsec - earlier->tv_nsec) / 1000000L; +} + +static long gap_ms_since_last(const struct timespec *now) +{ + long elapsed_ms; + + if (!g_have_last_tx) { + return (long)HYBBX_RF_TX_MIN_GAP_MS; + } + elapsed_ms = timespec_diff_ms(now, &g_last_tx); + if (elapsed_ms >= (long)HYBBX_RF_TX_MIN_GAP_MS) { + return 0; + } + return (long)HYBBX_RF_TX_MIN_GAP_MS - elapsed_ms; +} + +static void sleep_ms(long wait_ms) +{ + if (wait_ms > 0) { + usleep((useconds_t)wait_ms * 1000u); + } +} + +static void enforce_burst_limit(struct timespec *now) +{ + long since_burst; + long wait_ms; + + if (!g_have_last_tx) { + g_burst_start = *now; + return; + } + + if (gap_ms_since_last(now) == 0) { + g_burst_start = *now; + return; + } + + since_burst = timespec_diff_ms(now, &g_burst_start); + if (since_burst < (long)HYBBX_RF_TX_MAX_BURST_MS) { + return; + } + + wait_ms = gap_ms_since_last(now); + sleep_ms(wait_ms); + timespec_now(now); + g_burst_start = *now; +} + +static void mark_tx(const struct timespec *now) +{ + g_last_tx = *now; + g_have_last_tx = 1; +} + +void hybbx_rf_tx_burst_guard(void) +{ + struct timespec now; + + pthread_mutex_lock(&g_pace_lock); + timespec_now(&now); + enforce_burst_limit(&now); + mark_tx(&now); + pthread_mutex_unlock(&g_pace_lock); +} + +void hybbx_rf_tx_pace(void) +{ + struct timespec now; + long wait_ms; + + pthread_mutex_lock(&g_pace_lock); + timespec_now(&now); + enforce_burst_limit(&now); + wait_ms = gap_ms_since_last(&now); + sleep_ms(wait_ms); + timespec_now(&now); + mark_tx(&now); + pthread_mutex_unlock(&g_pace_lock); +} diff --git a/src/core/security.c b/src/core/security.c new file mode 100644 index 0000000..1a06600 --- /dev/null +++ b/src/core/security.c @@ -0,0 +1,205 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/security.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include + +#define HYBBX_SECURITY_LINE_MAX 512u + +static char g_security_dir[HYBBX_PATH_MAX]; +static int g_security_ready; +static FILE *g_security_file; +static pthread_mutex_t g_security_lock = PTHREAD_MUTEX_INITIALIZER; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + hybbx_strlcpy(buf, path, sizeof(buf)); + len = strlen(buf); + while (len > 0 && buf[len - 1] == '/') { + buf[--len] = '\0'; + } + + for (i = 1; i < len; i++) { + if (buf[i] != '/') { + continue; + } + buf[i] = '\0'; + if (buf[0] != '\0' && mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static int security_open_file(void) +{ + char path[HYBBX_PATH_MAX]; + + if (g_security_dir[0] == '\0') { + return -1; + } + + if (g_security_file != NULL) { + return 0; + } + + if (mkdir_p(g_security_dir) != 0) { + hybbx_log_warn("[security] cannot create directory %s", + g_security_dir); + return -1; + } + + if (hybbx_path_join(path, sizeof(path), g_security_dir, + HYBBX_SECURITY_LOG_FILE) != HYBBX_OK) { + return -1; + } + + g_security_file = fopen(path, "a"); + if (g_security_file == NULL) { + hybbx_log_warn("[security] cannot open %s", path); + return -1; + } + + return 0; +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_security_log_config_apply(const struct hybbx_config *config) +{ + const char *dir_raw; + + hybbx_security_log_shutdown(); + g_security_dir[0] = '\0'; + g_security_ready = 0; + + if (config != NULL) { + dir_raw = hybbx_config_get(config, "log", "dir", NULL); + if (dir_raw != NULL && dir_raw[0] != '\0') { + if (hybbx_path_resolve(g_security_dir, sizeof(g_security_dir), + dir_raw) != HYBBX_OK) { + hybbx_log_warn("[security] invalid log dir path"); + return; + } + } else { + if (hybbx_path_resolve(g_security_dir, sizeof(g_security_dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + hybbx_log_warn("[security] cannot resolve default log dir"); + return; + } + } + } else if (hybbx_path_resolve(g_security_dir, sizeof(g_security_dir), + HYBBX_DIR_LOGS) != HYBBX_OK) { + return; + } + + g_security_ready = 1; + + if (security_open_file() != 0) { + g_security_ready = 0; + return; + } + + hybbx_log_info("[security] log=%s/%s", g_security_dir, HYBBX_SECURITY_LOG_FILE); + hybbx_security_log_write("startup"); +} +#else +void hybbx_security_log_config_apply(const struct hybbx_config *config) +{ + (void)config; +} +#endif + +void hybbx_security_log_write(const char *fmt, ...) +{ + char message[HYBBX_SECURITY_LINE_MAX]; + char line[HYBBX_SECURITY_LINE_MAX + 64]; + char stamp[32]; + va_list ap; + time_t now; + struct tm tm_buf; + struct tm *tm; + + if (!g_security_ready || fmt == NULL) { + return; + } + + va_start(ap, fmt); + vsnprintf(message, sizeof(message), fmt, ap); + va_end(ap); + + now = time(NULL); + tm = localtime_r(&now, &tm_buf); + if (tm == NULL) { + return; + } + + if (strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", tm) == 0) { + return; + } + + snprintf(line, sizeof(line), "%s %s\n", stamp, message); + + pthread_mutex_lock(&g_security_lock); + + if (security_open_file() == 0) { + fputs(line, g_security_file); + fflush(g_security_file); + } + + pthread_mutex_unlock(&g_security_lock); +} + +hybbx_result_t hybbx_security_log_current_path(char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + out[0] = '\0'; + if (!g_security_ready || g_security_dir[0] == '\0') { + return HYBBX_ERR_NOT_FOUND; + } + + return hybbx_path_join(out, out_len, g_security_dir, HYBBX_SECURITY_LOG_FILE); +} + +void hybbx_security_log_shutdown(void) +{ + pthread_mutex_lock(&g_security_lock); + + if (g_security_file != NULL) { + fclose(g_security_file); + g_security_file = NULL; + } + + pthread_mutex_unlock(&g_security_lock); + g_security_ready = 0; +} diff --git a/src/core/security_ban.c b/src/core/security_ban.c new file mode 100644 index 0000000..8fc660f --- /dev/null +++ b/src/core/security_ban.c @@ -0,0 +1,1213 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/security_ban.h" +#include "hybbx/security.h" +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#include "hybbx/storage.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include +#include +#include +#include +#include +#include + +typedef struct hybbx_security_cfg { + int enabled; + unsigned maxretry; + unsigned findtime_sec; + unsigned bantime_sec; + unsigned abuse_maxretry; + unsigned abuse_findtime_sec; + int telnet; + int ssh; + int websocket; + int circuit; + unsigned rate_limit; + unsigned rate_window_sec; + hybbx_ban_backend_t backend; +} hybbx_security_cfg_t; + +typedef struct ban_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t expire_at; + int active; +} ban_entry_t; + +typedef struct fail_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t stamps[HYBBX_SECURITY_DEFAULT_MAXRETRY]; + unsigned count; + int active; +} fail_entry_t; + +typedef struct rate_entry { + char ip[HYBBX_REMOTE_ADDR_MAX]; + time_t stamps[32]; + unsigned count; + int active; +} rate_entry_t; + +typedef struct ban_callid_entry { + char callid[HYBBX_CALLID_MAX]; + time_t expire_at; + int active; + int permanent; +} ban_callid_entry_t; + +typedef struct callid_track_entry { + char callid[HYBBX_CALLID_MAX]; + time_t stamps[HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY]; + unsigned count; + int active; +} callid_track_entry_t; + +static hybbx_security_cfg_t g_cfg; +static ban_entry_t g_bans[HYBBX_SECURITY_BAN_MAX]; +static ban_callid_entry_t g_callid_bans[HYBBX_SECURITY_BAN_MAX]; +static fail_entry_t g_fails[HYBBX_SECURITY_TRACK_MAX]; +static fail_entry_t g_abuse[HYBBX_SECURITY_TRACK_MAX]; +static callid_track_entry_t g_callid_fails[HYBBX_SECURITY_TRACK_MAX]; +static callid_track_entry_t g_callid_abuse[HYBBX_SECURITY_TRACK_MAX]; +static rate_entry_t g_rates[HYBBX_SECURITY_TRACK_MAX]; +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; + +static void security_cfg_defaults(hybbx_security_cfg_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->enabled = 1; + cfg->maxretry = HYBBX_SECURITY_DEFAULT_MAXRETRY; + cfg->findtime_sec = HYBBX_SECURITY_DEFAULT_FINDTIME_SEC; + cfg->bantime_sec = HYBBX_SECURITY_DEFAULT_BANTIME_SEC; + cfg->abuse_maxretry = HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY; + cfg->abuse_findtime_sec = HYBBX_SECURITY_DEFAULT_ABUSE_FINDTIME_SEC; + cfg->telnet = 1; + cfg->ssh = 1; + cfg->websocket = 1; + cfg->circuit = 1; + cfg->rate_limit = HYBBX_SECURITY_DEFAULT_RATE_LIMIT; + cfg->rate_window_sec = HYBBX_SECURITY_DEFAULT_RATE_WINDOW_SEC; + cfg->backend = HYBBX_BAN_BACKEND_INTERNAL; +} + +static hybbx_ban_backend_t parse_ban_backend(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_BAN_BACKEND_INTERNAL; + } + + if (strcasecmp(value, "log") == 0) { + return HYBBX_BAN_BACKEND_LOG; + } + if (strcasecmp(value, "iptables") == 0) { + return HYBBX_BAN_BACKEND_IPTABLES; + } + if (strcasecmp(value, "nftables") == 0) { + return HYBBX_BAN_BACKEND_NFTABLES; + } + if (strcasecmp(value, "hosts") == 0) { + return HYBBX_BAN_BACKEND_HOSTS; + } + + return HYBBX_BAN_BACKEND_INTERNAL; +} + +static unsigned parse_uint_clamp(const char *value, unsigned default_value, + unsigned max_value) +{ + char *end = NULL; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return default_value; + } + + n = strtoul(value, &end, 10); + if (end == value || (end != NULL && *end != '\0')) { + return default_value; + } + + if (n > max_value) { + return max_value; + } + + return (unsigned)n; +} + +static int ip_valid(const char *ip) +{ + return ip != NULL && ip[0] != '\0' && strcmp(ip, "?") != 0; +} + +int hybbx_security_callid_normalize(const char *in, char *out, size_t out_cap) +{ + size_t len; + size_t i; + int has_alpha = 0; + + if (in == NULL || out == NULL || out_cap < 2u) { + return 0; + } + + while (*in == ' ' || *in == '\t') { + in++; + } + + len = 0; + for (i = 0; in[i] != '\0'; i++) { + unsigned char ch = (unsigned char)in[i]; + + if (ch == ' ' || ch == '\t') { + break; + } + + if (len + 1 >= out_cap) { + return 0; + } + + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { + out[len++] = (char)toupper(ch); + has_alpha = 1; + } else if (ch >= '0' && ch <= '9') { + out[len++] = (char)ch; + } else if (ch == '-' || ch == '_' || ch == '.') { + out[len++] = (char)ch; + } else { + return 0; + } + } + + out[len] = '\0'; + return len > 0 && has_alpha; +} + +static ban_callid_entry_t *callid_ban_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_callid_bans[i].active && + strcmp(g_callid_bans[i].callid, callid) == 0) { + return &g_callid_bans[i]; + } + } + + return NULL; +} + +static ban_callid_entry_t *callid_ban_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (!g_callid_bans[i].active) { + hybbx_strlcpy(g_callid_bans[i].callid, callid, + sizeof(g_callid_bans[i].callid)); + g_callid_bans[i].active = 1; + return &g_callid_bans[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_fail_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_callid_fails[i].active && + strcmp(g_callid_fails[i].callid, callid) == 0) { + return &g_callid_fails[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_fail_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_callid_fails[i].active) { + hybbx_strlcpy(g_callid_fails[i].callid, callid, + sizeof(g_callid_fails[i].callid)); + g_callid_fails[i].count = 0; + g_callid_fails[i].active = 1; + return &g_callid_fails[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_abuse_find(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_callid_abuse[i].active && + strcmp(g_callid_abuse[i].callid, callid) == 0) { + return &g_callid_abuse[i]; + } + } + + return NULL; +} + +static callid_track_entry_t *callid_abuse_alloc(const char *callid) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_callid_abuse[i].active) { + hybbx_strlcpy(g_callid_abuse[i].callid, callid, + sizeof(g_callid_abuse[i].callid)); + g_callid_abuse[i].count = 0; + g_callid_abuse[i].active = 1; + return &g_callid_abuse[i]; + } + } + + return NULL; +} + +static void prune_callid_fail_window(callid_track_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->callid[0] = '\0'; + } +} + +static void prune_callid_abuse_window(callid_track_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= + (time_t)g_cfg.abuse_findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->callid[0] = '\0'; + } +} + +static void backend_apply_callid(const char *callid, const char *reason) +{ + hybbx_security_log_write("ban callid=%s reason=%s backend=internal", + callid, reason != NULL ? reason : "abuse"); + (void)g_cfg.backend; +} + +static void apply_callid_ban_locked(const char *callid, const char *reason, + time_t now, int permanent) +{ + ban_callid_entry_t *ban; + + ban = callid_ban_find(callid); + if (ban == NULL) { + ban = callid_ban_alloc(callid); + } + + if (ban == NULL) { + return; + } + + ban->permanent = permanent ? 1 : 0; + ban->expire_at = permanent ? (time_t)0 : + now + (time_t)g_cfg.bantime_sec; + backend_apply_callid(callid, reason); +} + +static void config_clear_permanent_callid_bans_locked(void) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_callid_bans[i].active && g_callid_bans[i].permanent) { + g_callid_bans[i].active = 0; + g_callid_bans[i].callid[0] = '\0'; + g_callid_bans[i].permanent = 0; + } + } +} + +static void config_load_callid_bans_locked(const char *list) +{ + char buf[HYBBX_PATH_MAX]; + char norm[HYBBX_CALLID_MAX]; + char *save = NULL; + char *token; + + if (list == NULL || list[0] == '\0') { + return; + } + + hybbx_strlcpy(buf, list, sizeof(buf)); + token = strtok_r(buf, ",", &save); + while (token != NULL) { + while (*token == ' ' || *token == '\t') { + token++; + } + if (hybbx_security_callid_normalize(token, norm, sizeof(norm))) { + apply_callid_ban_locked(norm, "config", time(NULL), 1); + } + token = strtok_r(NULL, ",", &save); + } +} + +static void record_callid_failure_locked(const char *callid, time_t now) +{ + callid_track_entry_t *entry; + + entry = callid_fail_find(callid); + if (entry == NULL) { + entry = callid_fail_alloc(callid); + } + + if (entry == NULL) { + return; + } + + prune_callid_fail_window(entry, now); + + if (!entry->active) { + entry = callid_fail_alloc(callid); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.maxretry) { + apply_callid_ban_locked(callid, "link_auth_fail", now, 0); + entry->active = 0; + entry->count = 0; + entry->callid[0] = '\0'; + } +} + +static void record_callid_abuse_locked(const char *callid, const char *category, + time_t now) +{ + callid_track_entry_t *entry; + char reason[64]; + + entry = callid_abuse_find(callid); + if (entry == NULL) { + entry = callid_abuse_alloc(callid); + } + + if (entry == NULL) { + return; + } + + prune_callid_abuse_window(entry, now); + + if (!entry->active) { + entry = callid_abuse_alloc(callid); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.abuse_maxretry) { + snprintf(reason, sizeof(reason), "abuse:%s", + category != NULL && category[0] != '\0' ? category : "flood"); + apply_callid_ban_locked(callid, reason, now, 0); + entry->active = 0; + entry->count = 0; + entry->callid[0] = '\0'; + } +} + +static int callid_is_banned_locked(const char *callid, time_t now) +{ + ban_callid_entry_t *ban; + + ban = callid_ban_find(callid); + if (ban == NULL) { + return 0; + } + + if (ban->permanent) { + return 1; + } + + if (now < ban->expire_at) { + return 1; + } + + ban->active = 0; + ban->callid[0] = '\0'; + ban->permanent = 0; + return 0; +} + +static int transport_enabled(const char *transport) +{ + if (!g_cfg.enabled || transport == NULL || transport[0] == '\0') { + return 0; + } + + if (strcmp(transport, "telnet") == 0) { + return g_cfg.telnet; + } + if (strcmp(transport, "ssh") == 0) { + return g_cfg.ssh; + } + if (strcmp(transport, "websocket") == 0) { + return g_cfg.websocket; + } + if (strcmp(transport, "circuit") == 0) { + return g_cfg.circuit; + } + + return 1; +} + +static ban_entry_t *ban_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_bans[i].active && strcmp(g_bans[i].ip, ip) == 0) { + return &g_bans[i]; + } + } + + return NULL; +} + +static ban_entry_t *ban_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (!g_bans[i].active) { + hybbx_strlcpy(g_bans[i].ip, ip, sizeof(g_bans[i].ip)); + g_bans[i].active = 1; + return &g_bans[i]; + } + } + + return NULL; +} + +static fail_entry_t *fail_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_fails[i].active && strcmp(g_fails[i].ip, ip) == 0) { + return &g_fails[i]; + } + } + + return NULL; +} + +static fail_entry_t *fail_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_fails[i].active) { + hybbx_strlcpy(g_fails[i].ip, ip, sizeof(g_fails[i].ip)); + g_fails[i].count = 0; + g_fails[i].active = 1; + return &g_fails[i]; + } + } + + return NULL; +} + +static fail_entry_t *abuse_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_abuse[i].active && strcmp(g_abuse[i].ip, ip) == 0) { + return &g_abuse[i]; + } + } + + return NULL; +} + +static fail_entry_t *abuse_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_abuse[i].active) { + hybbx_strlcpy(g_abuse[i].ip, ip, sizeof(g_abuse[i].ip)); + g_abuse[i].count = 0; + g_abuse[i].active = 1; + return &g_abuse[i]; + } + } + + return NULL; +} + +static rate_entry_t *rate_find(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_rates[i].active && strcmp(g_rates[i].ip, ip) == 0) { + return &g_rates[i]; + } + } + + return NULL; +} + +static rate_entry_t *rate_alloc(const char *ip) +{ + size_t i; + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (!g_rates[i].active) { + hybbx_strlcpy(g_rates[i].ip, ip, sizeof(g_rates[i].ip)); + g_rates[i].count = 0; + g_rates[i].active = 1; + return &g_rates[i]; + } + } + + return NULL; +} + +static void prune_fail_window(fail_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static void prune_abuse_window(fail_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= + (time_t)g_cfg.abuse_findtime_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static void prune_rate_window(rate_entry_t *entry, time_t now) +{ + unsigned i; + unsigned kept = 0; + + if (entry == NULL) { + return; + } + + for (i = 0; i < entry->count; i++) { + if ((time_t)(now - entry->stamps[i]) <= (time_t)g_cfg.rate_window_sec) { + entry->stamps[kept++] = entry->stamps[i]; + } + } + + entry->count = kept; + if (entry->count == 0) { + entry->active = 0; + entry->ip[0] = '\0'; + } +} + +static int run_backend_cmd(const char *cmd) +{ + int rc; + + if (cmd == NULL || cmd[0] == '\0') { + return -1; + } + + rc = system(cmd); + return rc; +} + +static void backend_apply(const char *ip, const char *reason) +{ + char cmd[HYBBX_PATH_MAX]; + + switch (g_cfg.backend) { + case HYBBX_BAN_BACKEND_LOG: + hybbx_security_log_write("ban ip=%s reason=%s backend=log", + ip, reason != NULL ? reason : "abuse"); + break; + + case HYBBX_BAN_BACKEND_IPTABLES: + snprintf(cmd, sizeof(cmd), + "iptables -I INPUT -s %s -j DROP 2>/dev/null", ip); + if (run_backend_cmd(cmd) != 0) { + hybbx_security_log_write( + "ban_backend_fail ip=%s backend=iptables", ip); + } else { + hybbx_security_log_write( + "ban ip=%s reason=%s backend=iptables", + ip, reason != NULL ? reason : "abuse"); + } + break; + + case HYBBX_BAN_BACKEND_NFTABLES: + snprintf(cmd, sizeof(cmd), + "nft add rule inet filter input ip saddr %s drop " + "2>/dev/null", + ip); + if (run_backend_cmd(cmd) != 0) { + hybbx_security_log_write( + "ban_backend_fail ip=%s backend=nftables", ip); + } else { + hybbx_security_log_write( + "ban ip=%s reason=%s backend=nftables", + ip, reason != NULL ? reason : "abuse"); + } + break; + + case HYBBX_BAN_BACKEND_HOSTS: + hybbx_security_log_write( + "ban ip=%s reason=%s backend=hosts (stub — internal only)", + ip, reason != NULL ? reason : "abuse"); + break; + + case HYBBX_BAN_BACKEND_INTERNAL: + default: + hybbx_security_log_write("ban ip=%s reason=%s backend=internal", + ip, reason != NULL ? reason : "abuse"); + break; + } +} + +static void apply_ban_locked(const char *ip, const char *reason, time_t now) +{ + ban_entry_t *ban; + + ban = ban_find(ip); + if (ban == NULL) { + ban = ban_alloc(ip); + } + + if (ban == NULL) { + return; + } + + ban->expire_at = now + (time_t)g_cfg.bantime_sec; + backend_apply(ip, reason); +} + +static void record_failure_locked(const char *ip, time_t now) +{ + fail_entry_t *entry; + + entry = fail_find(ip); + if (entry == NULL) { + entry = fail_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_fail_window(entry, now); + + if (!entry->active) { + entry = fail_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.maxretry) { + apply_ban_locked(ip, "login_fail", now); + entry->active = 0; + entry->count = 0; + entry->ip[0] = '\0'; + } +} + +static void record_abuse_locked(const char *ip, const char *category, time_t now) +{ + fail_entry_t *entry; + char reason[64]; + + entry = abuse_find(ip); + if (entry == NULL) { + entry = abuse_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_abuse_window(entry, now); + + if (!entry->active) { + entry = abuse_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY) { + entry->stamps[entry->count++] = now; + } else { + memmove(entry->stamps, entry->stamps + 1, + (entry->count - 1) * sizeof(entry->stamps[0])); + entry->stamps[entry->count - 1] = now; + } + + if (entry->count >= g_cfg.abuse_maxretry) { + snprintf(reason, sizeof(reason), "abuse:%s", + category != NULL && category[0] != '\0' ? category : "flood"); + apply_ban_locked(ip, reason, now); + entry->active = 0; + entry->count = 0; + entry->ip[0] = '\0'; + } +} + +static void record_rate_locked(const char *ip, time_t now) +{ + rate_entry_t *entry; + + entry = rate_find(ip); + if (entry == NULL) { + entry = rate_alloc(ip); + } + + if (entry == NULL) { + return; + } + + prune_rate_window(entry, now); + + if (!entry->active) { + entry = rate_alloc(ip); + if (entry == NULL) { + return; + } + } + + if (entry->count < (unsigned)(sizeof(entry->stamps) / sizeof(entry->stamps[0]))) { + entry->stamps[entry->count++] = now; + } +} + +void hybbx_security_ban_config_apply(const struct hybbx_config *config) +{ + const char *value; + + pthread_mutex_lock(&g_lock); + + security_cfg_defaults(&g_cfg); + + if (config != NULL) { + value = hybbx_config_get(config, "security", "enabled", NULL); + if (value != NULL) { + g_cfg.enabled = hybbx_parse_bool(value, g_cfg.enabled); + } + + value = hybbx_config_get(config, "security", "maxretry", NULL); + g_cfg.maxretry = parse_uint_clamp(value, g_cfg.maxretry, 100u); + if (g_cfg.maxretry < 1u) { + g_cfg.maxretry = 1u; + } + + value = hybbx_config_get(config, "security", "findtime", NULL); + g_cfg.findtime_sec = + parse_uint_clamp(value, g_cfg.findtime_sec, 86400u); + + value = hybbx_config_get(config, "security", "bantime", NULL); + g_cfg.bantime_sec = + parse_uint_clamp(value, g_cfg.bantime_sec, 86400u); + + value = hybbx_config_get(config, "security", "abuse_maxretry", NULL); + g_cfg.abuse_maxretry = + parse_uint_clamp(value, g_cfg.abuse_maxretry, 1000u); + if (g_cfg.abuse_maxretry < 1u) { + g_cfg.abuse_maxretry = 1u; + } + + value = hybbx_config_get(config, "security", "abuse_findtime", NULL); + g_cfg.abuse_findtime_sec = + parse_uint_clamp(value, g_cfg.abuse_findtime_sec, 86400u); + + value = hybbx_config_get(config, "security", "telnet", NULL); + if (value != NULL) { + g_cfg.telnet = hybbx_parse_bool(value, g_cfg.telnet); + } + + value = hybbx_config_get(config, "security", "ssh", NULL); + if (value != NULL) { + g_cfg.ssh = hybbx_parse_bool(value, g_cfg.ssh); + } + + value = hybbx_config_get(config, "security", "websocket", NULL); + if (value != NULL) { + g_cfg.websocket = hybbx_parse_bool(value, g_cfg.websocket); + } + + value = hybbx_config_get(config, "security", "circuit", NULL); + if (value != NULL) { + g_cfg.circuit = hybbx_parse_bool(value, g_cfg.circuit); + } + + value = hybbx_config_get(config, "security", "rate_limit", NULL); + g_cfg.rate_limit = + parse_uint_clamp(value, g_cfg.rate_limit, 10000u); + + value = hybbx_config_get(config, "security", "rate_window", NULL); + g_cfg.rate_window_sec = + parse_uint_clamp(value, g_cfg.rate_window_sec, 3600u); + + value = hybbx_config_get(config, "security", "ban_backend", NULL); + g_cfg.backend = parse_ban_backend(value); + + config_clear_permanent_callid_bans_locked(); + value = hybbx_config_get(config, "security", "ban_callid", NULL); + config_load_callid_bans_locked(value); + } + + pthread_mutex_unlock(&g_lock); + + if (g_cfg.enabled) { + hybbx_log_info("[security] ban enabled maxretry=%u findtime=%us bantime=%us " + "abuse_maxretry=%u abuse_findtime=%us " + "rate_limit=%u/%us backend=%d", + g_cfg.maxretry, g_cfg.findtime_sec, g_cfg.bantime_sec, + g_cfg.abuse_maxretry, g_cfg.abuse_findtime_sec, + g_cfg.rate_limit, g_cfg.rate_window_sec, (int)g_cfg.backend); + } +} + +void hybbx_security_ban_shutdown(void) +{ + pthread_mutex_lock(&g_lock); + + memset(g_bans, 0, sizeof(g_bans)); + memset(g_callid_bans, 0, sizeof(g_callid_bans)); + memset(g_fails, 0, sizeof(g_fails)); + memset(g_abuse, 0, sizeof(g_abuse)); + memset(g_callid_fails, 0, sizeof(g_callid_fails)); + memset(g_callid_abuse, 0, sizeof(g_callid_abuse)); + memset(g_rates, 0, sizeof(g_rates)); + security_cfg_defaults(&g_cfg); + + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_tick(void) +{ + time_t now = time(NULL); + size_t i; + + if (!g_cfg.enabled) { + return; + } + + pthread_mutex_lock(&g_lock); + + for (i = 0; i < HYBBX_SECURITY_BAN_MAX; i++) { + if (g_bans[i].active && now >= g_bans[i].expire_at) { + hybbx_security_log_write("unban ip=%s", g_bans[i].ip); + g_bans[i].active = 0; + g_bans[i].ip[0] = '\0'; + } + if (g_callid_bans[i].active && !g_callid_bans[i].permanent && + g_callid_bans[i].expire_at > (time_t)0 && + now >= g_callid_bans[i].expire_at) { + hybbx_security_log_write("unban callid=%s", g_callid_bans[i].callid); + g_callid_bans[i].active = 0; + g_callid_bans[i].callid[0] = '\0'; + g_callid_bans[i].permanent = 0; + } + } + + for (i = 0; i < HYBBX_SECURITY_TRACK_MAX; i++) { + if (g_fails[i].active) { + prune_fail_window(&g_fails[i], now); + } + if (g_abuse[i].active) { + prune_abuse_window(&g_abuse[i], now); + } + if (g_callid_fails[i].active) { + prune_callid_fail_window(&g_callid_fails[i], now); + } + if (g_callid_abuse[i].active) { + prune_callid_abuse_window(&g_callid_abuse[i], now); + } + if (g_rates[i].active) { + prune_rate_window(&g_rates[i], now); + } + } + + pthread_mutex_unlock(&g_lock); +} + +int hybbx_security_ban_is_banned(const char *ip) +{ + ban_entry_t *ban; + time_t now = time(NULL); + int banned = 0; + + if (!g_cfg.enabled || !ip_valid(ip)) { + return 0; + } + + pthread_mutex_lock(&g_lock); + + ban = ban_find(ip); + if (ban != NULL) { + if (now < ban->expire_at) { + banned = 1; + } else { + ban->active = 0; + ban->ip[0] = '\0'; + } + } + + pthread_mutex_unlock(&g_lock); + return banned; +} + +int hybbx_security_ban_accept(const char *ip) +{ + rate_entry_t *entry; + ban_entry_t *ban; + time_t now = time(NULL); + int allow = 1; + + if (!g_cfg.enabled || !ip_valid(ip)) { + return 1; + } + + pthread_mutex_lock(&g_lock); + + ban = ban_find(ip); + if (ban != NULL) { + if (now < ban->expire_at) { + allow = 0; + } else { + ban->active = 0; + ban->ip[0] = '\0'; + } + } + + if (allow && g_cfg.rate_limit > 0u && g_cfg.rate_window_sec > 0u) { + entry = rate_find(ip); + if (entry != NULL) { + prune_rate_window(entry, now); + } + + if (entry != NULL && entry->active && + entry->count >= g_cfg.rate_limit) { + apply_ban_locked(ip, "rate_limit", now); + allow = 0; + } else { + record_rate_locked(ip, now); + } + } + + pthread_mutex_unlock(&g_lock); + return allow; +} + +int hybbx_security_ban_accept_fd(int fd) +{ + char ip[HYBBX_REMOTE_ADDR_MAX]; + + if (fd < 0) { + return 1; + } + + if (hybbx_socket_peer_name(fd, ip, sizeof(ip)) != HYBBX_OK) { + return 1; + } + + return hybbx_security_ban_accept(ip); +} + +void hybbx_security_ban_login_fail(const char *ip, const char *transport) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip) || !transport_enabled(transport)) { + return; + } + + pthread_mutex_lock(&g_lock); + record_failure_locked(ip, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_link_auth_fail(const char *ip) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip) || !g_cfg.circuit) { + return; + } + + pthread_mutex_lock(&g_lock); + record_failure_locked(ip, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_abuse_report(const char *ip, const char *category) +{ + time_t now = time(NULL); + + if (!g_cfg.enabled || !ip_valid(ip)) { + return; + } + + pthread_mutex_lock(&g_lock); + record_abuse_locked(ip, category, now); + pthread_mutex_unlock(&g_lock); +} + +int hybbx_security_ban_callid_is_banned(const char *callid) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + int banned = 0; + + if (!g_cfg.enabled || !hybbx_security_callid_normalize(callid, norm, + sizeof(norm))) { + return 0; + } + + pthread_mutex_lock(&g_lock); + banned = callid_is_banned_locked(norm, now); + pthread_mutex_unlock(&g_lock); + return banned; +} + +int hybbx_security_ban_callid_accept(const char *callid) +{ + return !hybbx_security_ban_callid_is_banned(callid); +} + +void hybbx_security_ban_link_auth_fail_callid(const char *callid) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + + if (!g_cfg.enabled || !g_cfg.circuit || + !hybbx_security_callid_normalize(callid, norm, sizeof(norm))) { + return; + } + + pthread_mutex_lock(&g_lock); + record_callid_failure_locked(norm, now); + pthread_mutex_unlock(&g_lock); +} + +void hybbx_security_ban_callid_abuse_report(const char *callid, + const char *category) +{ + char norm[HYBBX_CALLID_MAX]; + time_t now = time(NULL); + + if (!g_cfg.enabled || !hybbx_security_callid_normalize(callid, norm, + sizeof(norm))) { + return; + } + + pthread_mutex_lock(&g_lock); + record_callid_abuse_locked(norm, category, now); + pthread_mutex_unlock(&g_lock); +} diff --git a/src/core/service.c b/src/core/service.c new file mode 100644 index 0000000..a8bd402 --- /dev/null +++ b/src/core/service.c @@ -0,0 +1,1270 @@ +/* + * Centralized daemon: INI apply, plugin lifecycle, HBX circuit hub, sessions, + * link registry prune. Wire protocols stay in plugins/ (telnet, packet_radio). + */ +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "storage_private.h" +#include "hybbx/registry.h" +#include "hybbx/config.h" +#include "hybbx/crypto_config.h" +#include "hybbx/traffic.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_bridge.h" +#include "hybbx/link.h" +#include "hybbx/auth.h" +#include "hybbx/storage.h" +#include "hybbx/texts.h" +#include "hybbx/chat.h" +#include "hybbx/mail.h" +#include "hybbx/broadcast.h" + +#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY +void hybbx_mains_proxy_plugin_tick(void); +#endif +#include "hybbx/networks.h" +#include "hybbx/instance.h" +#include "hybbx/log.h" +#include "hybbx/monitor.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" + +#include +#include +#include +#include +#include +#if defined(_WIN32) +#include +#else +#include +#endif + +#define HYBBX_MAX_ACTIVE_TRANSPORTS 8 + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +typedef struct active_transport { + const hybbx_transport_plugin_t *plugin; + int running; +} active_transport_t; + +typedef struct hybbx_attached_session { + struct hybbx_session *session; + struct hybbx_attached_session *next; +} hybbx_attached_session_t; + +#define HYBBX_DEFAULT_DATA_PATH "data" + +struct hybbx_service_internal { + char *name; + char config_path[HYBBX_PATH_MAX]; + char prompt[HYBBX_PROMPT_MAX]; + unsigned max_online; + unsigned guest_timeout_minutes; + int login_announce; + unsigned active_nodes; + pthread_mutex_t node_lock; + pthread_mutex_t session_lock; + hybbx_attached_session_t *sessions; + hybbx_storage_t *storage; + hybbx_auth_config_t auth; + hybbx_texts_config_t texts; + hybbx_chat_config_t chat; + hybbx_mail_config_t mail; + hybbx_broadcast_config_t broadcast; + hybbx_networks_config_t networks; + active_transport_t transports[HYBBX_MAX_ACTIVE_TRANSPORTS]; + size_t transport_count; + hybbx_circuit_hub_t *circuit_hub; + int running; + hybbx_shutdown_mode_t shutdown_mode; + char launch_binary[HYBBX_PATH_MAX]; + pthread_mutex_t guest_lock; + unsigned char guest_in_use[HYBBX_GUEST_NUMBER_MAX + 1]; +}; + +hybbx_service_t *hybbx_service_create(const char *name) +{ + struct hybbx_service_internal *svc; + + svc = calloc(1, sizeof(*svc)); + if (svc == NULL) { + return NULL; + } + + hybbx_auth_config_defaults(&svc->auth); + hybbx_texts_config_defaults(&svc->texts); + hybbx_chat_config_defaults(&svc->chat); + hybbx_mail_config_defaults(&svc->mail); + hybbx_networks_config_defaults(&svc->networks); + svc->max_online = HYBBX_DEFAULT_MAX_ONLINE; + svc->guest_timeout_minutes = HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES; + svc->login_announce = 0; + svc->active_nodes = 0; + pthread_mutex_init(&svc->node_lock, NULL); + pthread_mutex_init(&svc->session_lock, NULL); + pthread_mutex_init(&svc->guest_lock, NULL); + + svc->name = hybbx_strdup(name != NULL && name[0] != '\0' ? + name : HYBBX_DEFAULT_SERVICE_NAME); + if (svc->name == NULL) { + free(svc); + return NULL; + } + + return (hybbx_service_t *)svc; +} + +void hybbx_service_destroy(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + size_t i; + + if (svc == NULL) { + return; + } + + hybbx_service_stop(service); + + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_destroy(svc->circuit_hub); + svc->circuit_hub = NULL; + } + + for (i = 0; i < svc->transport_count; i++) { + if (svc->transports[i].running && + svc->transports[i].plugin->stop != NULL) { + svc->transports[i].plugin->stop(); + } + if (svc->transports[i].plugin->shutdown != NULL) { + svc->transports[i].plugin->shutdown(); + } + } + + if (svc->storage != NULL) { + hybbx_storage_close(svc->storage); + svc->storage = NULL; + } + + hybbx_monitor_shutdown(); + hybbx_log_shutdown(); + hybbx_security_log_shutdown(); + hybbx_security_ban_shutdown(); + + { + hybbx_attached_session_t *node = svc->sessions; + + while (node != NULL) { + hybbx_attached_session_t *next = node->next; + + free(node); + node = next; + } + svc->sessions = NULL; + } + + pthread_mutex_destroy(&svc->session_lock); + pthread_mutex_destroy(&svc->guest_lock); + pthread_mutex_destroy(&svc->node_lock); + free(svc->name); + free(svc); +} + +hybbx_storage_t *hybbx_service_get_storage(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return svc->storage; +} + +const hybbx_auth_config_t *hybbx_service_get_auth(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->auth; +} + +const hybbx_texts_config_t *hybbx_service_get_texts(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->texts; +} + +const hybbx_chat_config_t *hybbx_service_get_chat(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->chat; +} + +const hybbx_mail_config_t *hybbx_service_get_mail(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->mail; +} + +const hybbx_broadcast_config_t *hybbx_service_get_broadcast( + const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return &svc->broadcast; +} + +hybbx_circuit_hub_t *hybbx_service_circuit_hub(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return NULL; + } + + return svc->circuit_hub; +} + +const char *hybbx_service_get_prompt(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return ""; + } + + return svc->prompt; +} + +const char *hybbx_service_get_name(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL || svc->name == NULL || svc->name[0] == '\0') { + return HYBBX_DEFAULT_SERVICE_NAME; + } + + return svc->name; +} + +unsigned hybbx_service_max_online(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_DEFAULT_MAX_ONLINE; + } + + return svc->max_online; +} + +int hybbx_service_login_announce(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + return svc != NULL && svc->login_announce != 0; +} + +unsigned hybbx_service_active_nodes(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + unsigned count; + + if (svc == NULL) { + return 0; + } + + pthread_mutex_lock((pthread_mutex_t *)&svc->node_lock); + count = svc->active_nodes; + pthread_mutex_unlock((pthread_mutex_t *)&svc->node_lock); + + return count; +} + +unsigned hybbx_service_guest_timeout_seconds(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES * 60u; + } + + return svc->guest_timeout_minutes * 60u; +} + +hybbx_result_t hybbx_service_guest_assign(hybbx_service_t *service, + const char *guest_prefix, + hybbx_user_record_t *out, + unsigned *slot_out) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + unsigned slot; + + if (svc == NULL || out == NULL || slot_out == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&svc->guest_lock); + for (slot = 1; slot <= HYBBX_GUEST_NUMBER_MAX; slot++) { + if (!svc->guest_in_use[slot]) { + svc->guest_in_use[slot] = 1; + pthread_mutex_unlock(&svc->guest_lock); + + hybbx_guest_fill_record(guest_prefix, slot, out); + *slot_out = slot; + return HYBBX_OK; + } + } + pthread_mutex_unlock(&svc->guest_lock); + + return HYBBX_ERR_BUSY; +} + +void hybbx_service_guest_release(hybbx_service_t *service, unsigned slot) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL || slot < 1 || slot > HYBBX_GUEST_NUMBER_MAX) { + return; + } + + pthread_mutex_lock(&svc->guest_lock); + svc->guest_in_use[slot] = 0; + pthread_mutex_unlock(&svc->guest_lock); +} + +hybbx_result_t hybbx_service_acquire_node(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&svc->node_lock); + if (svc->active_nodes >= svc->max_online) { + pthread_mutex_unlock(&svc->node_lock); + return HYBBX_ERR_BUSY; + } + + svc->active_nodes++; + pthread_mutex_unlock(&svc->node_lock); + return HYBBX_OK; +} + +void hybbx_service_release_node(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return; + } + + pthread_mutex_lock(&svc->node_lock); + if (svc->active_nodes > 0) { + svc->active_nodes--; + } + pthread_mutex_unlock(&svc->node_lock); +} + +hybbx_result_t hybbx_service_attach_session(hybbx_service_t *service, + hybbx_session_t *session) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + hybbx_attached_session_t *node; + + if (svc == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + node = calloc(1, sizeof(*node)); + if (node == NULL) { + return HYBBX_ERR_NOMEM; + } + + node->session = session; + + pthread_mutex_lock(&svc->session_lock); + node->next = svc->sessions; + svc->sessions = node; + pthread_mutex_unlock(&svc->session_lock); + + return HYBBX_OK; +} + +void hybbx_service_detach_session(hybbx_service_t *service, + hybbx_session_t *session) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + hybbx_attached_session_t *prev; + hybbx_attached_session_t *node; + + if (svc == NULL || session == NULL) { + return; + } + + pthread_mutex_lock(&svc->session_lock); + prev = NULL; + node = svc->sessions; + while (node != NULL) { + if (node->session == session) { + if (prev != NULL) { + prev->next = node->next; + } else { + svc->sessions = node->next; + } + free(node); + break; + } + prev = node; + node = node->next; + } + pthread_mutex_unlock(&svc->session_lock); +} + +void hybbx_service_visit_sessions(hybbx_service_t *service, + hybbx_service_session_visit_fn fn, + void *userdata) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + hybbx_attached_session_t *node; + hybbx_session_t **snapshot = NULL; + size_t count = 0; + size_t i; + + if (svc == NULL || fn == NULL) { + return; + } + + /* + * Snapshot session pointers under session_lock, then invoke callbacks + * without holding the lock. Visitors may write to circuit sessions, + * which can re-enter via load-balance -> bandwidth policy. + */ + pthread_mutex_lock(&svc->session_lock); + for (node = svc->sessions; node != NULL; node = node->next) { + if (node->session != NULL) { + count++; + } + } + + if (count > 0) { + snapshot = calloc(count, sizeof(*snapshot)); + if (snapshot == NULL) { + hybbx_log_warn("[service] session snapshot OOM (%zu sessions) — skipped", + count); + } else { + i = 0; + for (node = svc->sessions; node != NULL; node = node->next) { + if (node->session != NULL) { + snapshot[i++] = node->session; + } + } + } + } + pthread_mutex_unlock(&svc->session_lock); + + if (snapshot == NULL) { + return; + } + + for (i = 0; i < count; i++) { + fn(snapshot[i], userdata); + } + free(snapshot); +} + +typedef struct find_registered_session_ctx { + uint64_t user_id; + hybbx_session_t *exclude; + hybbx_session_t *found; +} find_registered_session_ctx_t; + +static void find_registered_session_visitor(hybbx_session_t *session, + void *userdata) +{ + find_registered_session_ctx_t *ctx = (find_registered_session_ctx_t *)userdata; + const hybbx_session_record_t *rec; + + if (ctx == NULL || ctx->found != NULL || session == NULL || + session == ctx->exclude) { + return; + } + + if (!hybbx_session_logged_in(session) || hybbx_session_is_guest(session)) { + return; + } + + rec = hybbx_session_record(session); + if (rec == NULL || rec->user_id != ctx->user_id) { + return; + } + + ctx->found = session; +} + +hybbx_session_t *hybbx_service_find_registered_session( + hybbx_service_t *service, + uint64_t user_id, + hybbx_session_t *exclude) +{ + find_registered_session_ctx_t ctx; + + if (service == NULL || user_id == 0) { + return NULL; + } + + ctx.user_id = user_id; + ctx.exclude = exclude; + ctx.found = NULL; + hybbx_service_visit_sessions(service, find_registered_session_visitor, &ctx); + return ctx.found; +} + +static void service_apply_service(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + const char *prompt; + const char *max_online_value; + const char *nodes_value; + + svc->prompt[0] = '\0'; + + prompt = hybbx_config_get(config, "service", "prompt", NULL); + if (prompt != NULL && prompt[0] != '\0') { + hybbx_strlcpy(svc->prompt, prompt, sizeof(svc->prompt)); + } + + max_online_value = hybbx_config_get(config, "service", "max_online", NULL); + nodes_value = hybbx_config_get(config, "service", "nodes", NULL); + if (max_online_value != NULL && max_online_value[0] != '\0') { + svc->max_online = hybbx_config_get_uint(config, "service", "max_online", + HYBBX_DEFAULT_MAX_ONLINE, 1u, + 999u); + } else if (nodes_value != NULL && nodes_value[0] != '\0') { + svc->max_online = hybbx_config_get_uint(config, "service", "nodes", + HYBBX_DEFAULT_MAX_ONLINE, 1u, + 999u); + } else { + svc->max_online = HYBBX_DEFAULT_MAX_ONLINE; + } + + svc->login_announce = + hybbx_config_get_bool(config, "service", "login_announce", 0); + + hybbx_log_info("[service] max_online=%u login_announce=%s", + svc->max_online, + svc->login_announce ? "yes" : "no"); +} + +static int str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void service_apply_texts(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + const char *path; + char resolved[HYBBX_PATH_MAX]; + + hybbx_texts_config_defaults(&svc->texts); + path = hybbx_config_get(config, "texts", "path", HYBBX_DIR_TEXT); + if (hybbx_path_resolve(resolved, sizeof(resolved), path) == HYBBX_OK) { + hybbx_strlcpy(svc->texts.path, resolved, sizeof(svc->texts.path)); + } else if (path != NULL && path[0] != '\0') { + hybbx_strlcpy(svc->texts.path, path, sizeof(svc->texts.path)); + } +} + +static hybbx_storage_backend_kind_t parse_storage_backend(const char *value) +{ + if (value == NULL || str_ieq_local(value, "flatfile") || + str_ieq_local(value, "flat") || str_ieq_local(value, "files")) { + return HYBBX_STORAGE_FLATFILE; + } + + if (str_ieq_local(value, "sqlite")) { + return HYBBX_STORAGE_SQLITE; + } + + if (str_ieq_local(value, "mysql")) { + return HYBBX_STORAGE_MYSQL; + } + + if (str_ieq_local(value, "mariadb")) { + return HYBBX_STORAGE_MARIADB; + } + + return HYBBX_STORAGE_FLATFILE; +} + +static hybbx_result_t service_open_storage(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + hybbx_storage_options_t options; + hybbx_storage_sql_config_t sql_cfg; + const char *backend_str; + const char *path_raw; + char path_resolved[HYBBX_PATH_MAX]; + const char *guest_prefix; + hybbx_result_t rc; + + if (svc->storage != NULL) { + hybbx_storage_close(svc->storage); + svc->storage = NULL; + } + + backend_str = hybbx_config_get(config, "storage", "backend", "flatfile"); + path_raw = hybbx_config_get(config, "storage", "path", HYBBX_DEFAULT_DATA_PATH); + + rc = hybbx_path_resolve(path_resolved, sizeof(path_resolved), path_raw); + if (rc != HYBBX_OK) { + hybbx_log_warn("[storage] invalid path '%s'", + path_raw != NULL ? path_raw : ""); + return HYBBX_ERR_IO; + } + + guest_prefix = hybbx_config_get(config, "auth", "guest_prefix", NULL); + + options.backend = parse_storage_backend(backend_str); + options.path = path_resolved; + options.guest_prefix = guest_prefix != NULL ? guest_prefix : + svc->auth.guest_prefix; + options.sql_cfg = NULL; + + if (options.backend == HYBBX_STORAGE_SQLITE) { + hybbx_storage_sql_config_apply(&sql_cfg, config, path_resolved); + options.sql_cfg = &sql_cfg; + } + + svc->storage = hybbx_storage_open(&options); + if (svc->storage == NULL) { + hybbx_log_warn("[storage] cannot open '%s' (writable?)", + path_resolved); + return HYBBX_ERR_IO; + } + + hybbx_log_info("[storage] backend=%s path=%s", backend_str, path_resolved); + return HYBBX_OK; +} + +static void service_apply_auth(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + const char *prefix; + + hybbx_auth_config_defaults(&svc->auth); + svc->auth.auto_login = + hybbx_config_get_bool(config, "auth", "auto_login", 1); + + prefix = hybbx_config_get(config, "auth", "guest_prefix", NULL); + if (prefix != NULL && prefix[0] != '\0') { + hybbx_strlcpy(svc->auth.guest_prefix, prefix, + sizeof(svc->auth.guest_prefix)); + } + + svc->guest_timeout_minutes = hybbx_config_get_uint( + config, "auth", "guest_timeout_minutes", + HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES, 1u, 24u * 60u); + + hybbx_log_info("[service] guest_timeout_minutes=%u", svc->guest_timeout_minutes); +} + +static hybbx_result_t service_apply_circuit(struct hybbx_service_internal *svc, + const hybbx_config_t *config) +{ + hybbx_circuit_config_t cfg; + const char *bind4; + const char *bind6; + hybbx_result_t rc; + + if (!hybbx_config_get_bool(config, "circuit", "enabled", 1)) { + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_stop(svc->circuit_hub); + } + return HYBBX_OK; + } + + if (!svc->networks.circuit) { + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_stop(svc->circuit_hub); + } + return HYBBX_OK; + } + + hybbx_circuit_config_defaults(&cfg); + bind4 = hybbx_config_get(config, "circuit", "bind", NULL); + bind6 = hybbx_config_get(config, "circuit", "bind6", NULL); + if (bind4 != NULL && bind4[0] != '\0') { + hybbx_strlcpy(cfg.bind4, bind4, sizeof(cfg.bind4)); + } + if (bind6 != NULL && bind6[0] != '\0') { + hybbx_strlcpy(cfg.bind6, bind6, sizeof(cfg.bind6)); + } + + cfg.port = hybbx_config_get_uint(config, "circuit", "port", + HYBBX_CIRCUIT_DEFAULT_PORT, 1u, 65535u); + cfg.ipv4 = hybbx_config_get_bool(config, "circuit", "ipv4", 1); + cfg.ipv6 = hybbx_config_get_bool(config, "circuit", "ipv6", 1); + cfg.link_auth = hybbx_config_get_bool(config, "circuit", "link_auth", 1); + cfg.link_stale_days = hybbx_config_get_uint( + config, "circuit", "link_stale_days", HYBBX_LINK_STALE_DAYS, 1u, 365u); + cfg.balance.enabled = hybbx_config_get_bool(config, "circuit", "balance", 1); + cfg.balance.lag_sec = hybbx_config_get_uint( + config, "circuit", "balance_lag_sec", 8u, 1u, 120u); + cfg.balance.queue_pause = (size_t)hybbx_config_get_uint( + config, "circuit", "balance_queue_pause", 8192u, 256u, 1048576u); + cfg.balance.queue_break = (size_t)hybbx_config_get_uint( + config, "circuit", "balance_queue_break", 32768u, 1024u, 1048576u); + cfg.balance.queue_cancel = (size_t)hybbx_config_get_uint( + config, "circuit", "balance_queue_cancel", 131072u, 4096u, 4194304u); + if (cfg.balance.queue_pause > cfg.balance.queue_break) { + cfg.balance.queue_break = cfg.balance.queue_pause; + } + if (cfg.balance.queue_break > cfg.balance.queue_cancel) { + cfg.balance.queue_cancel = cfg.balance.queue_break; + } + cfg.max_links = hybbx_config_get_uint( + config, "circuit", "max_links", HYBBX_CIRCUIT_DEFAULT_MAX_LINKS, + 1u, HYBBX_CIRCUIT_MAX_LINKS); + (void)hybbx_circuit_bridge_load(&cfg.bridge, config); + + { + const char *link_pw = hybbx_config_get(config, "circuit", "link_password", NULL); + + if (link_pw != NULL) { + hybbx_strlcpy(cfg.link_password, link_pw, sizeof(cfg.link_password)); + } + if (svc->storage != NULL && svc->storage->path != NULL && + svc->storage->path[0] != '\0') { + hybbx_strlcpy(cfg.data_path, svc->storage->path, sizeof(cfg.data_path)); + } else { + char data_resolved[HYBBX_PATH_MAX]; + const char *path_raw = hybbx_config_get(config, "storage", "path", + HYBBX_DEFAULT_DATA_PATH); + + if (hybbx_path_resolve(data_resolved, sizeof(data_resolved), + path_raw) == HYBBX_OK) { + hybbx_strlcpy(cfg.data_path, data_resolved, sizeof(cfg.data_path)); + } + } + if (svc->config_path[0] != '\0') { + hybbx_strlcpy(cfg.config_path, svc->config_path, sizeof(cfg.config_path)); + } + } + + if (svc->circuit_hub == NULL) { + svc->circuit_hub = hybbx_circuit_hub_create((hybbx_service_t *)svc); + if (svc->circuit_hub == NULL) { + return HYBBX_ERR_NOMEM; + } + } + + rc = hybbx_circuit_hub_start(svc->circuit_hub, &cfg); + return rc; +} + +hybbx_result_t hybbx_service_load_transport(hybbx_service_t *service, + const char *plugin_name) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + const hybbx_transport_plugin_t *plugin; + size_t i; + + if (svc == NULL || plugin_name == NULL) { + return HYBBX_ERR_INVALID; + } + + plugin = hybbx_registry_find(plugin_name); + if (plugin == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + for (i = 0; i < svc->transport_count; i++) { + if (svc->transports[i].plugin == plugin) { + return HYBBX_OK; + } + } + + if (svc->transport_count >= HYBBX_MAX_ACTIVE_TRANSPORTS) { + return HYBBX_ERR_NOMEM; + } + + if (plugin->init != NULL) { + hybbx_result_t rc = plugin->init(service); + if (rc != HYBBX_OK) { + return rc; + } + } + + svc->transports[svc->transport_count].plugin = plugin; + svc->transports[svc->transport_count].running = 0; + svc->transport_count++; + return HYBBX_OK; +} + +static active_transport_t *find_active(struct hybbx_service_internal *svc, + const char *plugin_name) +{ + size_t i; + + for (i = 0; i < svc->transport_count; i++) { + if (strcmp(svc->transports[i].plugin->name, plugin_name) == 0) { + return &svc->transports[i]; + } + } + return NULL; +} + +hybbx_result_t hybbx_service_start_transport(hybbx_service_t *service, + const char *plugin_name, + const char *config) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + active_transport_t *active; + + if (svc == NULL || plugin_name == NULL) { + return HYBBX_ERR_INVALID; + } + + active = find_active(svc, plugin_name); + if (active == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (active->running) { + return HYBBX_ERR_BUSY; + } + + if (active->plugin->start == NULL) { + return HYBBX_ERR_UNSUPPORTED; + } + + { + hybbx_result_t rc = active->plugin->start(config); + if (rc != HYBBX_OK) { + return rc; + } + } + + active->running = 1; + return HYBBX_OK; +} + +hybbx_result_t hybbx_service_stop_transport(hybbx_service_t *service, + const char *plugin_name) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + active_transport_t *active; + + if (svc == NULL || plugin_name == NULL) { + return HYBBX_ERR_INVALID; + } + + active = find_active(svc, plugin_name); + if (active == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (!active->running) { + return HYBBX_OK; + } + + if (active->plugin->stop != NULL) { + hybbx_result_t rc = active->plugin->stop(); + if (rc != HYBBX_OK) { + return rc; + } + } + + active->running = 0; + return HYBBX_OK; +} + +hybbx_result_t hybbx_service_run(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_ERR_INVALID; + } + + svc->running = 1; + + while (svc->running) { +#if defined(_WIN32) + Sleep(1000); +#else + static unsigned prune_tick; + + sleep(1); + hybbx_security_ban_tick(); + hybbx_monitor_tick(service); + hybbx_broadcast_ax25_tick(service); +#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY + hybbx_mains_proxy_plugin_tick(); +#endif + if (svc->storage != NULL) { + hybbx_storage_backup_tick(svc->storage); + } + prune_tick++; + if (prune_tick >= 3600u) { + prune_tick = 0; + if (svc->circuit_hub != NULL) { + hybbx_circuit_hub_prune_links(svc->circuit_hub); + } + } +#endif + } + + return HYBBX_OK; +} + +void hybbx_service_stop(hybbx_service_t *service) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc != NULL) { + svc->running = 0; + } +} + +void hybbx_service_set_launch_binary(hybbx_service_t *service, const char *argv0) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return; + } + + svc->launch_binary[0] = '\0'; + if (argv0 != NULL && argv0[0] != '\0') { + hybbx_strlcpy(svc->launch_binary, argv0, sizeof(svc->launch_binary)); + } +} + +void hybbx_service_request_shutdown(hybbx_service_t *service, int restart) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + + if (svc == NULL) { + return; + } + + svc->shutdown_mode = restart ? HYBBX_SHUTDOWN_RESTART : HYBBX_SHUTDOWN_STOP; + hybbx_service_stop(service); +} + +hybbx_shutdown_mode_t hybbx_service_shutdown_mode(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL) { + return HYBBX_SHUTDOWN_NONE; + } + + return svc->shutdown_mode; +} + +const char *hybbx_service_config_path(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + + if (svc == NULL || svc->config_path[0] == '\0') { + return NULL; + } + + return svc->config_path; +} + +void hybbx_service_restart_exec(const hybbx_service_t *service) +{ + const struct hybbx_service_internal *svc = + (const struct hybbx_service_internal *)service; + const char *binary; + const char *config_path; + char *argv[4]; + char opt_c[] = "-c"; + + if (svc == NULL) { + return; + } + + binary = svc->launch_binary[0] != '\0' ? svc->launch_binary : HYBBX_DAEMON_BINARY; + config_path = svc->config_path[0] != '\0' ? svc->config_path : NULL; + + argv[0] = (char *)binary; + if (config_path != NULL) { + argv[1] = opt_c; + argv[2] = (char *)config_path; + argv[3] = NULL; + } else { + argv[1] = NULL; + } + + fflush(NULL); + execv(binary, argv); + perror("hybbx restart failed"); +} + +typedef struct apply_transport_ctx { + hybbx_service_t *service; + const hybbx_config_t *config; + const hybbx_networks_config_t *networks; + hybbx_result_t last_error; +} apply_transport_ctx_t; + +static int transport_start_failure_is_fatal(const char *plugin_name) +{ + /* Role static transport must start; others may fail independently. */ + return hybbx_networks_is_static_transport(plugin_name); +} + +static void apply_transport_cb(const hybbx_transport_plugin_t *plugin, + void *userdata) +{ + apply_transport_ctx_t *ctx = (apply_transport_ctx_t *)userdata; + char section[128]; + char *transport_config; + hybbx_result_t rc; + int transport_enabled; + + if (ctx->networks == NULL || + !hybbx_networks_transport_wanted(plugin->name, ctx->networks)) { + return; + } + + if (!hybbx_config_resolve_transport_section(ctx->config, plugin->name, + section, sizeof(section))) { + snprintf(section, sizeof(section), "transport.%s", plugin->name); + } + + if (hybbx_networks_is_static_transport(plugin->name)) { + transport_enabled = 1; + } else { + transport_enabled = hybbx_config_get_bool(ctx->config, section, + "enabled", 1); + } + + if (!transport_enabled) { + return; + } + + rc = hybbx_service_load_transport(ctx->service, plugin->name); + if (rc != HYBBX_OK) { + hybbx_log_warn("[service] %s: plugin load failed (%s)", + plugin->name, hybbx_result_name(rc)); + if (transport_start_failure_is_fatal(plugin->name)) { + ctx->last_error = rc; + } + return; + } + + if (strcmp(plugin->name, "packet_radio") == 0) { + transport_config = + hybbx_config_format_packet_radio_start(ctx->config); + } else if (strcmp(plugin->name, "baycom") == 0) { + transport_config = hybbx_config_format_baycom_start(ctx->config); + } else if (strcmp(plugin->name, "mains_proxy") == 0) { + transport_config = hybbx_config_format_transport_sections(ctx->config, + plugin->name); + } else { + transport_config = NULL; + } + if (transport_config == NULL) { + transport_config = hybbx_config_format_section(ctx->config, section); + } + rc = hybbx_service_start_transport(ctx->service, plugin->name, + transport_config); + free(transport_config); + + if (rc != HYBBX_OK) { + hybbx_log_warn("[service] %s: start failed (%s)", plugin->name, + hybbx_result_name(rc)); + if (transport_start_failure_is_fatal(plugin->name)) { + ctx->last_error = rc; + } + } +} + +hybbx_result_t hybbx_service_apply_config(hybbx_service_t *service, + const hybbx_config_t *config, + const char *config_path) +{ + struct hybbx_service_internal *svc = + (struct hybbx_service_internal *)service; + const char *service_name; + apply_transport_ctx_t ctx; + char *new_name; + hybbx_result_t rc; + + if (svc == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + svc->config_path[0] = '\0'; + if (config_path != NULL && config_path[0] != '\0') { + hybbx_strlcpy(svc->config_path, config_path, sizeof(svc->config_path)); + } + + service_name = hybbx_config_get(config, "service", "name", NULL); + if (service_name != NULL) { + new_name = hybbx_strdup(service_name); + if (new_name == NULL) { + return HYBBX_ERR_NOMEM; + } + free(svc->name); + svc->name = new_name; + } + + service_apply_auth(svc, config); + hybbx_time_config_apply(config); + hybbx_log_config_apply(config); + hybbx_security_log_config_apply(config); + hybbx_monitor_config_apply(config); + hybbx_security_ban_config_apply(config); + service_apply_texts(svc, config); + hybbx_chat_config_apply(&svc->chat, config); + hybbx_broadcast_config_apply(&svc->broadcast, config); + service_apply_service(svc, config); + hybbx_crypto_config_apply(config); + hybbx_traffic_config_apply(config); + + rc = service_open_storage(svc, config); + if (rc != HYBBX_OK) { + return rc; + } + + { + const char *storage_path = HYBBX_DEFAULT_DATA_PATH; + char storage_resolved[HYBBX_PATH_MAX]; + + if (svc->storage != NULL && svc->storage->path != NULL && + svc->storage->path[0] != '\0') { + storage_path = svc->storage->path; + } else if (hybbx_path_resolve(storage_resolved, sizeof(storage_resolved), + storage_path) == HYBBX_OK) { + storage_path = storage_resolved; + } + hybbx_mail_config_apply(&svc->mail, config, storage_path); + } + + hybbx_networks_config_apply(&svc->networks, config); + + { + int standalone = hybbx_config_get_bool(config, "instance", "standalone", 0); + + if (!standalone && hybbx_instance_role() == HYBBX_INSTANCE_MAIN) { + if (svc->networks.ax25 || svc->networks.baycom || + svc->networks.ardop || svc->networks.crdop) { + standalone = 1; + } + } + hybbx_instance_set_standalone(standalone); + } + + rc = hybbx_networks_enforce_instance(&svc->networks); + if (rc != HYBBX_OK) { + return rc; + } + + rc = service_apply_circuit(svc, config); + if (rc != HYBBX_OK) { + return rc; + } + + ctx.service = service; + ctx.config = config; + ctx.networks = &svc->networks; + ctx.last_error = HYBBX_OK; + + hybbx_registry_foreach(apply_transport_cb, &ctx); + return ctx.last_error; +} diff --git a/src/core/session.c b/src/core/session.c new file mode 100644 index 0000000..9f5d368 --- /dev/null +++ b/src/core/session.c @@ -0,0 +1,2678 @@ +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/command.h" +#include "hybbx/storage.h" +#include "hybbx/texts.h" +#include "hybbx/auth.h" +#include "hybbx/chat.h" +#include "hybbx/conference.h" +#include "hybbx/mail.h" +#include "hybbx/proxymail.h" +#include "hybbx/proxychat.h" +#include "hybbx/terminal.h" +#include "hybbx/traffic.h" +#include "hybbx/log.h" +#include "hybbx/monitor.h" +#include "hybbx/instance.h" +#include "hybbx/hybbx.h" +#include "hybbx/util.h" +#include "hybbx/bandwidth_policy.h" +#include "hybbx/instance.h" +#include "hybbx/messages.h" + +#include +#include +#include +#include +#include + +static int session_str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +typedef struct hybbx_session_core { + hybbx_session_t pub; + hybbx_session_record_t record; + hybbx_user_record_t user; + hybbx_service_t *service; + char line_buf[HYBBX_LINE_MAX]; + size_t line_len; + size_t line_cursor; + char history[HYBBX_HISTORY_MAX][HYBBX_LINE_MAX]; + unsigned history_count; + unsigned history_next; + int history_view; + char history_saved_line[HYBBX_LINE_MAX]; + int expect_lf; + int logged_in; + int login_prompt; + unsigned guest_slot; + time_t guest_expires_at; + hybbx_session_area_t area; + hybbx_session_area_t area_stack[HYBBX_AREA_STACK_MAX]; + unsigned area_depth; + unsigned chat_channel; + int chat_max_notice_shown; + char conference_topic[HYBBX_CONFERENCE_TOPIC_MAX]; + char conference_partner[HYBBX_USER_NAME_MAX]; + int conference_invite_pending; + char conference_invite_from[HYBBX_USER_NAME_MAX]; + char conference_invite_topic[HYBBX_CONFERENCE_TOPIC_MAX]; + time_t conference_invite_deadline; + struct { + char target[HYBBX_USER_NAME_MAX]; + time_t sent_at[HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET]; + } conference_invite_rates[8]; + int monitor_active; + int mail_composing; + char mail_compose_to[HYBBX_USER_NAME_MAX]; + char mail_compose_subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + char mail_compose_body[HYBBX_MAIL_BODY_MAX + 1]; + size_t mail_compose_body_len; + int proxymail_composing; + char proxymail_compose_to[HYBBX_PROXYMAIL_ADDRESS_MAX]; + char proxymail_compose_subject[HYBBX_MAIL_SUBJECT_MAX + 1]; + char proxymail_compose_body[HYBBX_MAIL_BODY_MAX + 1]; + size_t proxymail_compose_body_len; + unsigned out_col; + int input_echo; + int bandwidth_paused; + int bandwidth_disconnect; + unsigned char esc_state; + char csi_buf[24]; + size_t csi_len; +} hybbx_session_core_t; + +#define SESSION_ESC_NONE 0u +#define SESSION_ESC_ESC 1u +#define SESSION_ESC_CSI 2u +#define SESSION_ESC_SS3 3u + +#define SESSION_CSI_BUF_MAX 24u + +static hybbx_result_t session_handle_user_byte(hybbx_session_core_t *core, + unsigned char byte); + +static size_t session_line_max_len(const hybbx_session_core_t *core); +static unsigned session_chat_message_max(const hybbx_session_core_t *core); +static hybbx_result_t session_write_transport(hybbx_session_core_t *core, + const char *data, size_t len); +static hybbx_result_t session_line_refresh_suffix(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_left(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_right(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_home(hybbx_session_core_t *core); +static hybbx_result_t session_line_cursor_end(hybbx_session_core_t *core); +static hybbx_result_t session_line_backspace(hybbx_session_core_t *core); +static hybbx_result_t session_line_delete_forward(hybbx_session_core_t *core); +static hybbx_result_t session_line_insert_char(hybbx_session_core_t *core, + char ch); +static hybbx_result_t session_history_up(hybbx_session_core_t *core); +static hybbx_result_t session_history_down(hybbx_session_core_t *core); +static void session_maybe_announce_login(hybbx_session_core_t *core); + +static void session_escape_reset(hybbx_session_core_t *core) +{ + if (core == NULL) { + return; + } + + core->esc_state = SESSION_ESC_NONE; + core->csi_len = 0; +} + +static int session_csi_is_final(unsigned char ch) +{ + return ch >= 0x40u && ch <= 0x7eu; +} + +static size_t session_line_max_len(const hybbx_session_core_t *core) +{ + size_t line_max = sizeof(core->line_buf) - 1; + + if (core == NULL) { + return HYBBX_LINE_MAX - 1; + } + + if (core->area == HYBBX_AREA_CHAT || core->area == HYBBX_AREA_CONFERENCE || + core->area == HYBBX_AREA_PROXYCHAT) { + line_max = session_chat_message_max(core); + } else if (core->area == HYBBX_AREA_MAIL && core->mail_composing) { + line_max = HYBBX_LINE_MAX - 1; + } else if (core->area == HYBBX_AREA_PROXYMAIL && core->proxymail_composing) { + line_max = HYBBX_LINE_MAX - 1; + } + + return line_max; +} + +static int session_char_echoable(char ch) +{ + unsigned char byte = (unsigned char)ch; + + return byte >= 0x20 && byte != 0x7f; +} + +static hybbx_result_t session_write_transport(hybbx_session_core_t *core, + const char *data, size_t len) +{ + if (core == NULL || data == NULL || len == 0) { + return HYBBX_OK; + } + + if (core->pub.transport != NULL && core->pub.transport->write != NULL) { + return core->pub.transport->write(&core->pub, data, len); + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_move_back(hybbx_session_core_t *core, + size_t count) +{ + char buf[64]; + size_t chunk; + size_t sent = 0; + + if (core == NULL || count == 0) { + return HYBBX_OK; + } + + memset(buf, '\b', sizeof(buf)); + + while (sent < count) { + hybbx_result_t rc; + + chunk = count - sent; + if (chunk > sizeof(buf)) { + chunk = sizeof(buf); + } + + rc = session_write_transport(core, buf, chunk); + if (rc != HYBBX_OK) { + return rc; + } + sent += chunk; + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_refresh_suffix(hybbx_session_core_t *core) +{ + size_t suffix_len; + hybbx_result_t rc; + + if (core == NULL || !core->input_echo) { + return HYBBX_OK; + } + + suffix_len = core->line_len - core->line_cursor; + if (suffix_len > 0) { + rc = session_write_transport(core, core->line_buf + core->line_cursor, + suffix_len); + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = session_write_transport(core, "\x1b[K", 3); + if (rc != HYBBX_OK) { + return rc; + } + + return session_line_move_back(core, suffix_len); +} + +static hybbx_result_t session_line_cursor_left(hybbx_session_core_t *core) +{ + if (core == NULL || core->line_cursor == 0) { + return HYBBX_OK; + } + + core->line_cursor--; + if (!core->input_echo) { + return HYBBX_OK; + } + + return session_write_transport(core, "\b", 1); +} + +static hybbx_result_t session_line_cursor_right(hybbx_session_core_t *core) +{ + hybbx_result_t rc; + + if (core == NULL || core->line_cursor >= core->line_len) { + return HYBBX_OK; + } + + if (core->input_echo) { + char out[2]; + + out[0] = core->line_buf[core->line_cursor]; + out[1] = '\0'; + rc = session_write_transport(core, out, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + + core->line_cursor++; + return HYBBX_OK; +} + +static hybbx_result_t session_line_cursor_home(hybbx_session_core_t *core) +{ + hybbx_result_t rc = HYBBX_OK; + + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + while (core->line_cursor > 0) { + rc = session_line_cursor_left(core); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_cursor_end(hybbx_session_core_t *core) +{ + hybbx_result_t rc = HYBBX_OK; + + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + while (core->line_cursor < core->line_len) { + rc = session_line_cursor_right(core); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t session_line_backspace(hybbx_session_core_t *core) +{ + hybbx_result_t rc; + + if (core == NULL || core->line_cursor == 0) { + return HYBBX_OK; + } + + core->line_cursor--; + memmove(core->line_buf + core->line_cursor, + core->line_buf + core->line_cursor + 1, + core->line_len - core->line_cursor - 1); + core->line_len--; + core->line_buf[core->line_len] = '\0'; + + if (!core->input_echo) { + return HYBBX_OK; + } + + rc = session_write_transport(core, "\b", 1); + if (rc != HYBBX_OK) { + return rc; + } + + return session_line_refresh_suffix(core); +} + +static hybbx_result_t session_line_delete_forward(hybbx_session_core_t *core) +{ + if (core == NULL || core->line_cursor >= core->line_len) { + return HYBBX_OK; + } + + memmove(core->line_buf + core->line_cursor, + core->line_buf + core->line_cursor + 1, + core->line_len - core->line_cursor - 1); + core->line_len--; + core->line_buf[core->line_len] = '\0'; + + if (!core->input_echo) { + return HYBBX_OK; + } + + return session_line_refresh_suffix(core); +} + +static hybbx_result_t session_line_insert_char(hybbx_session_core_t *core, + char ch) +{ + size_t line_max; + hybbx_result_t rc; + char out[2]; + + if (core == NULL || !session_char_echoable(ch)) { + return HYBBX_OK; + } + + line_max = session_line_max_len(core); + if (core->line_len >= line_max) { + return HYBBX_OK; + } + + if (core->line_cursor < core->line_len) { + memmove(core->line_buf + core->line_cursor + 1, + core->line_buf + core->line_cursor, + core->line_len - core->line_cursor); + } + + core->line_buf[core->line_cursor] = ch; + core->line_len++; + core->line_cursor++; + core->line_buf[core->line_len] = '\0'; + + if (!core->input_echo) { + return HYBBX_OK; + } + + out[0] = ch; + out[1] = '\0'; + rc = session_write_transport(core, out, 1); + if (rc != HYBBX_OK) { + core->line_cursor--; + core->line_len--; + core->line_buf[core->line_len] = '\0'; + return rc; + } + + return session_line_refresh_suffix(core); +} + +static int session_history_should_store(const char *line) +{ + if (line == NULL || line[0] == '\0') { + return 0; + } + + return strncmp(line, "/login ", 7) != 0 && + strncmp(line, "/register ", 10) != 0 && + strncmp(line, "/changeme ", 10) != 0 && + strncmp(line, "/userchange ", 12) != 0; +} + +static void session_history_add(hybbx_session_core_t *core, const char *line) +{ + if (core == NULL || !session_history_should_store(line)) { + return; + } + + hybbx_strlcpy(core->history[core->history_next], line, + sizeof(core->history[0])); + core->history_next = (core->history_next + 1u) % HYBBX_HISTORY_MAX; + if (core->history_count < HYBBX_HISTORY_MAX) { + core->history_count++; + } + core->history_view = -1; + core->history_saved_line[0] = '\0'; +} + +static hybbx_result_t session_line_replace(hybbx_session_core_t *core, + const char *line) +{ + size_t len; + hybbx_result_t rc; + + if (core == NULL || line == NULL) { + return HYBBX_ERR_INVALID; + } + + len = strlen(line); + if (len >= sizeof(core->line_buf)) { + len = sizeof(core->line_buf) - 1; + } + + if (core->input_echo) { + rc = session_line_move_back(core, core->line_cursor); + if (rc != HYBBX_OK) { + return rc; + } + if (len > 0) { + rc = session_write_transport(core, line, len); + if (rc != HYBBX_OK) { + return rc; + } + } + rc = session_write_transport(core, "\x1b[K", 3); + if (rc != HYBBX_OK) { + return rc; + } + } + + memcpy(core->line_buf, line, len); + core->line_buf[len] = '\0'; + core->line_len = len; + core->line_cursor = len; + return HYBBX_OK; +} + +static hybbx_result_t session_history_up(hybbx_session_core_t *core) +{ + unsigned slot; + + if (core == NULL || core->history_count == 0) { + return HYBBX_OK; + } + + if (core->history_view < 0) { + hybbx_strlcpy(core->history_saved_line, core->line_buf, + sizeof(core->history_saved_line)); + core->history_view = 0; + } else if ((unsigned)(core->history_view + 1) < core->history_count) { + core->history_view++; + } else { + return HYBBX_OK; + } + + slot = (core->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)core->history_view) % HYBBX_HISTORY_MAX; + return session_line_replace(core, core->history[slot]); +} + +static hybbx_result_t session_history_down(hybbx_session_core_t *core) +{ + unsigned slot; + + if (core == NULL || core->history_view < 0) { + return HYBBX_OK; + } + + if (core->history_view == 0) { + core->history_view = -1; + return session_line_replace(core, core->history_saved_line); + } + + core->history_view--; + slot = (core->history_next + HYBBX_HISTORY_MAX - 1u - + (unsigned)core->history_view) % HYBBX_HISTORY_MAX; + return session_line_replace(core, core->history[slot]); +} + +static hybbx_result_t session_apply_csi(hybbx_session_core_t *core, + const char *params, char final_ch) +{ + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (final_ch == 'A') { + return session_history_up(core); + } + + if (final_ch == 'B') { + return session_history_down(core); + } + + if (final_ch == 'C') { + return session_line_cursor_right(core); + } + + if (final_ch == 'D') { + return session_line_cursor_left(core); + } + + if (final_ch == 'H') { + return session_line_cursor_home(core); + } + + if (final_ch == 'F') { + return session_line_cursor_end(core); + } + + if (final_ch == '~') { + if (strcmp(params, "1") == 0 || strcmp(params, "7") == 0) { + return session_line_cursor_home(core); + } + if (strcmp(params, "3") == 0) { + return session_line_delete_forward(core); + } + if (strcmp(params, "4") == 0 || strcmp(params, "8") == 0) { + return session_line_cursor_end(core); + } + return HYBBX_OK; + } + + if (final_ch == 'K' || final_ch == 'P') { + return HYBBX_OK; + } + + return HYBBX_OK; +} + +static int session_filter_escape_byte(hybbx_session_core_t *core, unsigned char byte) +{ + if (core == NULL) { + return 0; + } + + if (core->esc_state == SESSION_ESC_NONE && byte == 0x9bu) { + core->esc_state = SESSION_ESC_CSI; + core->csi_len = 0; + return 1; + } + + if (core->esc_state == SESSION_ESC_NONE && byte == 0x1bu) { + core->esc_state = SESSION_ESC_ESC; + return 1; + } + + if (core->esc_state == SESSION_ESC_ESC) { + if (byte == '[') { + core->esc_state = SESSION_ESC_CSI; + core->csi_len = 0; + return 1; + } + if (byte == 'O') { + core->esc_state = SESSION_ESC_SS3; + return 1; + } + + session_escape_reset(core); + return 0; + } + + if (core->esc_state == SESSION_ESC_SS3) { + hybbx_result_t rc = HYBBX_OK; + + if (byte == 'C') { + rc = session_line_cursor_right(core); + } else if (byte == 'D') { + rc = session_line_cursor_left(core); + } else if (byte == 'H') { + rc = session_line_cursor_home(core); + } else if (byte == 'F') { + rc = session_line_cursor_end(core); + } + + session_escape_reset(core); + (void)rc; + return 1; + } + + if (core->esc_state == SESSION_ESC_CSI) { + if (core->csi_len + 1 >= SESSION_CSI_BUF_MAX) { + session_escape_reset(core); + return 1; + } + + core->csi_buf[core->csi_len++] = (char)byte; + + if (session_csi_is_final(byte)) { + char final_ch = (char)byte; + char params[SESSION_CSI_BUF_MAX]; + + if (core->csi_len > 1) { + memcpy(params, core->csi_buf, core->csi_len - 1); + params[core->csi_len - 1] = '\0'; + } else { + params[0] = '\0'; + } + + session_escape_reset(core); + (void)session_apply_csi(core, params, final_ch); + return 1; + } + + return 1; + } + + return 0; +} + +static unsigned session_chat_message_max(const hybbx_session_core_t *core) +{ + const hybbx_chat_config_t *chat; + + if (core == NULL || core->service == NULL) { + return HYBBX_CHAT_MESSAGE_MAX; + } + + chat = hybbx_service_get_chat(core->service); + if (chat == NULL) { + return HYBBX_CHAT_MESSAGE_MAX; + } + + return chat->message_max; +} + +static void session_area_clear_state(hybbx_session_core_t *core, + hybbx_session_area_t area) +{ + if (core == NULL) { + return; + } + + if (area == HYBBX_AREA_CHAT) { + core->chat_channel = 0; + } + + if (area == HYBBX_AREA_CONFERENCE) { + hybbx_conference_area_leaving(&core->pub); + } + + if (area == HYBBX_AREA_MAIL) { + core->mail_composing = 0; + core->mail_compose_body[0] = '\0'; + core->mail_compose_body_len = 0; + core->mail_compose_to[0] = '\0'; + core->mail_compose_subject[0] = '\0'; + } + + if (area == HYBBX_AREA_PROXYMAIL) { + core->proxymail_composing = 0; + core->proxymail_compose_body[0] = '\0'; + core->proxymail_compose_body_len = 0; + core->proxymail_compose_to[0] = '\0'; + core->proxymail_compose_subject[0] = '\0'; + } +} + +static void session_area_sync(hybbx_session_core_t *core) +{ + if (core == NULL || core->area_depth == 0) { + return; + } + + core->area = core->area_stack[core->area_depth - 1]; +} + +static hybbx_result_t session_area_push(hybbx_session_core_t *core, + hybbx_session_area_t area) +{ + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area_depth >= HYBBX_AREA_STACK_MAX) { + return HYBBX_ERR_BUSY; + } + + if (core->area_depth > 0 && + core->area_stack[core->area_depth - 1] == area) { + core->area = area; + return HYBBX_OK; + } + + core->area_stack[core->area_depth] = area; + core->area_depth++; + core->area = area; + return HYBBX_OK; +} + +static void session_release_guest_slot(hybbx_session_core_t *core) +{ + if (core == NULL || core->guest_slot == 0 || core->service == NULL) { + return; + } + + hybbx_service_guest_release(core->service, core->guest_slot); + core->guest_slot = 0; +} + +static void session_arm_guest_timer(hybbx_session_core_t *core) +{ + unsigned timeout_sec; + + if (core == NULL || !hybbx_user_level_is_guest(core->user.level)) { + if (core != NULL) { + core->guest_expires_at = 0; + } + return; + } + + timeout_sec = hybbx_service_guest_timeout_seconds(core->service); + if (timeout_sec == 0) { + core->guest_expires_at = 0; + return; + } + + core->guest_expires_at = time(NULL) + (time_t)timeout_sec; +} + +static hybbx_result_t session_activate_guest(hybbx_session_core_t *core, + const hybbx_user_record_t *guest, + unsigned guest_slot) +{ + hybbx_storage_t *storage; + const char *transport_name; + hybbx_result_t rc; + + if (core == NULL || guest == NULL || guest_slot < 1) { + return HYBBX_ERR_INVALID; + } + + storage = hybbx_service_get_storage(core->service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->guest_slot != 0 && core->guest_slot != guest_slot) { + session_release_guest_slot(core); + } + + transport_name = core->pub.transport != NULL ? + core->pub.transport->name : "unknown"; + + if (core->record.session_id != 0) { + hybbx_storage_session_end(storage, core->record.session_id); + memset(&core->record, 0, sizeof(core->record)); + } + + rc = hybbx_storage_session_begin(storage, guest, transport_name, + &core->record); + if (rc != HYBBX_OK) { + hybbx_service_guest_release(core->service, guest_slot); + return rc; + } + + core->user = *guest; + core->guest_slot = guest_slot; + core->logged_in = 1; + core->login_prompt = 0; + session_arm_guest_timer(core); + return HYBBX_OK; +} + +static hybbx_result_t session_check_guest_expiry(hybbx_session_core_t *core) +{ + time_t now; + + if (core == NULL || core->guest_expires_at == 0) { + return HYBBX_OK; + } + + now = time(NULL); + if (now < core->guest_expires_at) { + return HYBBX_OK; + } + + (void)hybbx_msg_send_system(&core->pub, "Guest time limit. Goodbye."); + return HYBBX_SESSION_END; +} + +static void session_process_line(hybbx_session_core_t *core, const char *line); + +hybbx_result_t hybbx_session_write(hybbx_session_t *session, const char *text) +{ + hybbx_session_core_t *core; + + if (session == NULL || text == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core != NULL && core->bandwidth_paused) { + return HYBBX_OK; + } + + return hybbx_traffic_emit(session, + core != NULL ? &core->out_col : NULL, + text, strlen(text)); +} + +hybbx_result_t hybbx_session_write_line(hybbx_session_t *session, + const char *text) +{ + hybbx_result_t rc; + + if (text != NULL) { + rc = hybbx_session_write(session, text); + if (rc != HYBBX_OK) { + return rc; + } + } + + return hybbx_session_write(session, "\n"); +} + +hybbx_result_t hybbx_session_command_gap(hybbx_session_t *session) +{ + return hybbx_session_write(session, "\n"); +} + +static int session_accepts_input(const hybbx_session_core_t *core) +{ + return core != NULL && (core->logged_in != 0 || core->login_prompt != 0); +} + +hybbx_result_t hybbx_session_show_prompt(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + const char *prompt; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!core->logged_in && !core->login_prompt) { + return HYBBX_ERR_INVALID; + } + + prompt = hybbx_service_get_prompt(core->service); + if (prompt == NULL || prompt[0] == '\0') { + return HYBBX_OK; + } + + return hybbx_session_write(session, prompt); +} + +hybbx_result_t hybbx_session_clear_terminal(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !session_accepts_input(core)) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_term_clear_screen(session); + if (rc != HYBBX_OK) { + return rc; + } + + core->line_len = 0; + core->line_cursor = 0; + core->line_buf[0] = '\0'; + core->out_col = 0; + core->expect_lf = 0; + + return hybbx_session_show_prompt(session); +} + +int hybbx_session_input_echo(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->input_echo != 0; +} + +hybbx_result_t hybbx_session_set_input_echo(hybbx_session_t *session, int enabled) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !session_accepts_input(core)) { + return HYBBX_ERR_INVALID; + } + + core->input_echo = enabled ? 1 : 0; + return HYBBX_OK; +} + +static void session_submit_line(hybbx_session_core_t *core) +{ + core->line_buf[core->line_len] = '\0'; + session_history_add(core, core->line_buf); + session_process_line(core, core->line_buf); + core->line_len = 0; + core->line_cursor = 0; + core->history_view = -1; + if ((core->logged_in || core->login_prompt) && + core->area != HYBBX_AREA_CHAT && + core->area != HYBBX_AREA_CONFERENCE && + core->area != HYBBX_AREA_PROXYCHAT && + !(core->area == HYBBX_AREA_MAIL && core->mail_composing) && + !(core->area == HYBBX_AREA_PROXYMAIL && core->proxymail_composing)) { + hybbx_session_show_prompt(&core->pub); + } +} + +static hybbx_result_t session_echo_newline(hybbx_session_core_t *core) +{ + if (core == NULL || !core->input_echo) { + return HYBBX_OK; + } + + return hybbx_session_write(&core->pub, "\n"); +} + +static hybbx_result_t session_handle_user_byte(hybbx_session_core_t *core, + unsigned char byte) +{ + char ch = (char)byte; + + if (ch == '\0') { + return HYBBX_OK; + } + + if (ch == '\b' || ch == 127) { + return session_line_backspace(core); + } + + if (ch == '\r') { + { + hybbx_result_t rc = session_echo_newline(core); + + if (rc != HYBBX_OK) { + return rc; + } + } + session_submit_line(core); + if (!core->logged_in && !core->login_prompt) { + return HYBBX_SESSION_END; + } + core->expect_lf = 1; + return HYBBX_OK; + } + + if (ch == '\n') { + if (core->expect_lf) { + core->expect_lf = 0; + return HYBBX_OK; + } + + { + hybbx_result_t rc = session_echo_newline(core); + + if (rc != HYBBX_OK) { + return rc; + } + } + + session_submit_line(core); + if (!core->logged_in && !core->login_prompt) { + return HYBBX_SESSION_END; + } + return HYBBX_OK; + } + + core->expect_lf = 0; + + return session_line_insert_char(core, ch); +} + +static void session_send_banner(hybbx_session_core_t *core) +{ + const hybbx_texts_config_t *texts; + const char *service_name; + + texts = hybbx_service_get_texts(core->service); + if (texts == NULL) { + return; + } + + service_name = hybbx_service_get_name(core->service); + hybbx_texts_send_banner(texts, &core->pub, HYBBX_VERSION_STRING, service_name); +} + +static void session_process_line(hybbx_session_core_t *core, const char *line) +{ + hybbx_parsed_command_t cmd; + hybbx_command_scope_t scope; + hybbx_result_t rc; + + if (line[0] == '\0') { + return; + } + + scope = hybbx_command_classify(line); + if (scope == HYBBX_CMD_SCOPE_COMMENT) { + return; + } + + if (hybbx_conference_invite_pending(&core->pub) && + scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_conference_reply_invite(core->service, &core->pub, line) == + HYBBX_OK) { + return; + } + hybbx_session_write_line(&core->pub, "Reply y/n or yes/no."); + return; + } + + if (core->area == HYBBX_AREA_MAIL && core->mail_composing && + scope == HYBBX_CMD_SCOPE_LOCAL) { + const hybbx_mail_config_t *mail; + size_t line_len; + size_t body_max; + + mail = hybbx_service_get_mail(core->service); + body_max = mail != NULL ? mail->body_max : HYBBX_MAIL_BODY_MAX; + line_len = strlen(line); + + if (core->mail_compose_body_len + line_len + 2 > body_max) { + hybbx_session_write_line(&core->pub, "Message body too long."); + return; + } + + if (core->mail_compose_body_len > 0) { + core->mail_compose_body[core->mail_compose_body_len++] = '\n'; + } + memcpy(core->mail_compose_body + core->mail_compose_body_len, + line, line_len + 1); + core->mail_compose_body_len += line_len; + return; + } + + if (core->area == HYBBX_AREA_PROXYMAIL && core->proxymail_composing && + scope == HYBBX_CMD_SCOPE_LOCAL) { + const hybbx_mail_config_t *mail; + size_t line_len; + size_t body_max; + + mail = hybbx_service_get_mail(core->service); + body_max = mail != NULL ? mail->body_max : HYBBX_MAIL_BODY_MAX; + line_len = strlen(line); + + if (core->proxymail_compose_body_len + line_len + 2 > body_max) { + hybbx_session_write_line(&core->pub, "Message body too long."); + return; + } + + if (core->proxymail_compose_body_len > 0) { + core->proxymail_compose_body[core->proxymail_compose_body_len++] = + '\n'; + } + memcpy(core->proxymail_compose_body + core->proxymail_compose_body_len, + line, line_len + 1); + core->proxymail_compose_body_len += line_len; + return; + } + + if (core->area == HYBBX_AREA_PROXYCHAT && scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_session_is_guest(&core->pub)) { + hybbx_session_write_line(&core->pub, "Guests cannot use proxychat."); + return; + } + + if (strlen(line) > session_chat_message_max(core)) { + hybbx_session_write_line(&core->pub, "Message too long."); + return; + } + + (void)hybbx_proxychat_post(hybbx_session_service(&core->pub), &core->pub, + line); + return; + } + + if (core->area == HYBBX_AREA_CHAT && scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_session_is_guest(&core->pub)) { + hybbx_session_write_line(&core->pub, "Guests cannot use chat."); + return; + } + + if (strlen(line) > session_chat_message_max(core)) { + hybbx_session_write_line(&core->pub, "Message too long."); + return; + } + + (void)hybbx_chat_post(core->service, &core->pub, line); + return; + } + + if (core->area == HYBBX_AREA_CONFERENCE && scope == HYBBX_CMD_SCOPE_LOCAL) { + if (hybbx_session_is_guest(&core->pub)) { + hybbx_session_write_line(&core->pub, "Guests cannot use conference."); + return; + } + + if (strlen(line) > session_chat_message_max(core)) { + hybbx_session_write_line(&core->pub, "Message too long."); + return; + } + + (void)hybbx_conference_post(core->service, &core->pub, line); + return; + } + + if (scope == HYBBX_CMD_SCOPE_LOCAL) { + return; + } + + rc = hybbx_command_parse(line, &cmd); + if (rc != HYBBX_OK) { + hybbx_session_write_line(&core->pub, "Command parse error."); + return; + } + + rc = hybbx_command_dispatch(core->service, &core->pub, &cmd); + if (rc == HYBBX_SESSION_END) { + core->logged_in = 0; + } else if (rc == HYBBX_ERR_DENIED) { + /* access message already sent */ + } else if (rc != HYBBX_OK && rc != HYBBX_ERR_NOT_FOUND) { + hybbx_session_write_line(&core->pub, "Command failed."); + } + + hybbx_command_free(&cmd); +} + +hybbx_result_t hybbx_session_open(hybbx_service_t *service, + const hybbx_transport_plugin_t *transport, + void *transport_data, + hybbx_session_t **out) +{ + hybbx_session_core_t *core; + hybbx_storage_t *storage; + const hybbx_auth_config_t *auth; + hybbx_result_t rc; + int suppress_startup_text; + int interactive; + + if (service == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + interactive = (transport->kind == HYBBX_TRANSPORT_TELNET || + transport->kind == HYBBX_TRANSPORT_SSH || + transport->kind == HYBBX_TRANSPORT_WEBSOCKET); + + if (interactive && !hybbx_instance_offers_user_bbx()) { + hybbx_log_warn("[session] reject %s on %s (no user BBX on this instance)", + transport->name, hybbx_instance_role_name()); + return HYBBX_ERR_DENIED; + } + + rc = hybbx_service_acquire_node(service); + if (rc != HYBBX_OK) { + return rc; + } + + storage = hybbx_service_get_storage(service); + if (storage == NULL) { + hybbx_service_release_node(service); + return HYBBX_ERR_INVALID; + } + + auth = hybbx_service_get_auth(service); + if (auth == NULL) { + hybbx_service_release_node(service); + return HYBBX_ERR_INVALID; + } + + core = calloc(1, sizeof(*core)); + if (core == NULL) { + hybbx_service_release_node(service); + return HYBBX_ERR_NOMEM; + } + + core->service = service; + core->pub.transport = transport; + core->pub.transport_data = transport_data; + core->pub.core_data = core; + core->area = HYBBX_AREA_MAIN; + core->area_stack[0] = HYBBX_AREA_MAIN; + core->area_depth = 1; + + { + const hybbx_traffic_config_t *traffic = hybbx_traffic_config_get(); + + core->input_echo = traffic != NULL && traffic->input_echo; + } + + suppress_startup_text = (transport->kind == HYBBX_TRANSPORT_CIRCUIT); + + (void)hybbx_term_init_session(&core->pub); + + if (!suppress_startup_text) { + session_send_banner(core); + } + + if (auth->auto_login) { + unsigned guest_slot = 0; + + rc = hybbx_service_guest_assign(service, auth->guest_prefix, + &core->user, &guest_slot); + if (rc != HYBBX_OK) { + free(core); + hybbx_service_release_node(service); + return rc; + } + + rc = session_activate_guest(core, &core->user, guest_slot); + if (rc != HYBBX_OK) { + free(core); + hybbx_service_release_node(service); + return rc; + } + + if (!suppress_startup_text) { + hybbx_log_info("%s@%s connected (session %llu)", + core->record.username, transport->name, + (unsigned long long)core->record.session_id); + hybbx_monitor_event(service, core->record.username, transport->name, + "connected"); + } + + if (!suppress_startup_text) { + hybbx_session_show_prompt(&core->pub); + } + } else { + core->login_prompt = 1; + if (!suppress_startup_text) { + hybbx_session_show_prompt(&core->pub); + } + } + + *out = &core->pub; + + if (hybbx_service_attach_session(service, &core->pub) != HYBBX_OK) { + if (core->record.session_id != 0) { + hybbx_storage_session_end(storage, core->record.session_id); + } + session_release_guest_slot(core); + free(core); + hybbx_service_release_node(service); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} + +void hybbx_session_close(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_storage_t *storage; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + if (hybbx_conference_invite_pending(session) || + hybbx_session_area(session) == HYBBX_AREA_CONFERENCE) { + hybbx_conference_session_closed(session); + } + + storage = hybbx_service_get_storage(core->service); + if (storage != NULL && core->record.session_id != 0) { + const char *plugin = core->record.transport[0] != '\0' + ? core->record.transport + : (session->transport != NULL + ? session->transport->name + : "?"); + hybbx_storage_session_end(storage, core->record.session_id); + hybbx_log_info("%s@%s disconnected (session %llu)", + core->record.username, plugin, + (unsigned long long)core->record.session_id); + hybbx_monitor_event(core->service, core->record.username, plugin, + "disconnected"); + } + + session_release_guest_slot(core); + + if (core->service != NULL) { + hybbx_service_detach_session(core->service, session); + hybbx_service_release_node(core->service); + } + + free(core); +} + +hybbx_result_t hybbx_session_switch_user(hybbx_session_t *session, + const hybbx_user_record_t *user) +{ + hybbx_session_core_t *core; + hybbx_storage_t *storage; + hybbx_session_record_t new_record; + const char *transport_name; + hybbx_result_t rc; + + if (session == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_user_level_is_guest(user->level)) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || core->service == NULL) { + return HYBBX_ERR_INVALID; + } + + session_release_guest_slot(core); + + storage = hybbx_service_get_storage(core->service); + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + transport_name = core->record.transport; + if (transport_name[0] == '\0' && session->transport != NULL) { + transport_name = session->transport->name; + } + if (transport_name == NULL || transport_name[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (core->record.session_id != 0) { + hybbx_storage_session_end(storage, core->record.session_id); + } + + rc = hybbx_storage_session_begin(storage, user, transport_name, &new_record); + if (rc != HYBBX_OK) { + return rc; + } + + core->user = *user; + core->record = new_record; + core->guest_expires_at = 0; + core->logged_in = 1; + core->login_prompt = 0; + + hybbx_log_info("%s@%s login (session %llu)", + core->record.username, transport_name, + (unsigned long long)core->record.session_id); + hybbx_monitor_event(core->service, core->record.username, transport_name, + "login"); + + session_maybe_announce_login(core); + + return HYBBX_OK; +} + +hybbx_result_t hybbx_session_tick(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->logged_in) { + return HYBBX_ERR_INVALID; + } + + if (core->bandwidth_disconnect) { + return HYBBX_SESSION_END; + } + + if (core->conference_invite_pending && + core->conference_invite_deadline > 0) { + hybbx_conference_invite_tick(core->service, session); + } + + return session_check_guest_expiry(core); +} + +hybbx_result_t hybbx_session_handle_input(hybbx_session_t *session, + const uint8_t *data, size_t len) +{ + hybbx_session_core_t *core; + size_t i; + hybbx_result_t expiry_rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !session_accepts_input(core)) { + return HYBBX_ERR_INVALID; + } + + if (core->bandwidth_paused) { + return HYBBX_OK; + } + + if (core->logged_in) { + expiry_rc = session_check_guest_expiry(core); + if (expiry_rc != HYBBX_OK) { + return expiry_rc; + } + } + + for (i = 0; i < len; i++) { + hybbx_result_t rc; + + if (session_filter_escape_byte(core, data[i])) { + continue; + } + + rc = session_handle_user_byte(core, data[i]); + + if (rc == HYBBX_SESSION_END) { + return HYBBX_SESSION_END; + } + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +const char *hybbx_session_display_name(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return ""; + } + + return hybbx_user_display_name(&core->user); +} + +hybbx_result_t hybbx_session_format_user_at_plugin(const hybbx_session_t *session, + char *out, size_t out_len) +{ + const hybbx_session_record_t *rec; + const char *user; + const char *plugin; + + if (session == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + user = hybbx_session_display_name(session); + if (user == NULL || user[0] == '\0') { + user = hybbx_session_username(session); + } + if (user == NULL || user[0] == '\0') { + user = "?"; + } + + rec = hybbx_session_record(session); + plugin = (rec != NULL && rec->transport[0] != '\0') ? rec->transport : NULL; + if (plugin == NULL || plugin[0] == '\0') { + plugin = (session->transport != NULL && session->transport->name != NULL) + ? session->transport->name + : "?"; + } + + snprintf(out, out_len, "%s@%s", user, plugin); + return HYBBX_OK; +} + +typedef struct login_announce_ctx { + const char *line; +} login_announce_ctx_t; + +static void login_announce_visitor(hybbx_session_t *session, void *userdata) +{ + login_announce_ctx_t *ctx = (login_announce_ctx_t *)userdata; + + if (session == NULL || ctx == NULL || ctx->line == NULL) { + return; + } + + if (!hybbx_session_is_interactive_user(session) || + !hybbx_session_logged_in(session)) { + return; + } + + hybbx_session_write_line(session, ctx->line); +} + +static void session_maybe_announce_login(hybbx_session_core_t *core) +{ + hybbx_service_t *service; + char user_at[96]; + char line[128]; + login_announce_ctx_t ctx; + + if (core == NULL || core->service == NULL) { + return; + } + + service = core->service; + if (!hybbx_service_login_announce(service)) { + return; + } + + /* Guests: never announce. Hidden Sysop (invisible-sysop / monitor): skip. */ + if (hybbx_user_level_is_guest(core->user.level) || + hybbx_session_hidden_from_who(&core->pub)) { + return; + } + + if (hybbx_session_format_user_at_plugin(&core->pub, user_at, + sizeof(user_at)) != HYBBX_OK) { + return; + } + + snprintf(line, sizeof(line), "*** User login: %s", user_at); + ctx.line = line; + hybbx_service_visit_sessions(service, login_announce_visitor, &ctx); +} + +const char *hybbx_session_username(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return ""; + } + + return core->record.username; +} + +uint64_t hybbx_session_id(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->record.session_id; +} + +const hybbx_session_record_t *hybbx_session_record(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return NULL; + } + + return &core->record; +} + +hybbx_result_t hybbx_session_set_remote(hybbx_session_t *session, + const char *remote) +{ + hybbx_session_core_t *core; + + if (session == NULL || remote == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(core->record.remote, remote, sizeof(core->record.remote)); + return HYBBX_OK; +} + +hybbx_user_level_t hybbx_session_user_level(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_LEVEL_GUEST; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_LEVEL_GUEST; + } + + return core->user.level; +} + +int hybbx_session_is_guest(const hybbx_session_t *session) +{ + return hybbx_user_level_is_guest(hybbx_session_user_level(session)); +} + +int hybbx_session_is_interactive_user(const hybbx_session_t *session) +{ + hybbx_transport_kind_t kind; + + if (session == NULL || session->transport == NULL) { + return 0; + } + + kind = session->transport->kind; + return kind == HYBBX_TRANSPORT_TELNET || + kind == HYBBX_TRANSPORT_SSH || + kind == HYBBX_TRANSPORT_WEBSOCKET; +} + +int hybbx_session_monitor_active(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + return core != NULL && core->monitor_active != 0; +} + +void hybbx_session_set_monitor_active(hybbx_session_t *session, int on) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->monitor_active = on ? 1 : 0; +} + +int hybbx_session_hidden_from_who(const hybbx_session_t *session) +{ + if (session == NULL) { + return 0; + } + + if (!hybbx_user_level_is_sysop(hybbx_session_user_level(session))) { + return 0; + } + + if (hybbx_monitor_invisible_sysop()) { + return 1; + } + + return hybbx_session_monitor_active(session) ? 1 : 0; +} + +int hybbx_session_logged_in(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->logged_in != 0; +} + +int hybbx_session_login_prompt(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->login_prompt != 0; +} + +hybbx_session_area_t hybbx_session_area(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_AREA_MAIN; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_AREA_MAIN; + } + + return core->area; +} + +const char *hybbx_session_area_name(hybbx_session_area_t area) +{ + switch (area) { + case HYBBX_AREA_MAIN: + return "main"; + case HYBBX_AREA_MAIL: + return "mail"; + case HYBBX_AREA_CHAT: + return "chat"; + case HYBBX_AREA_CONFERENCE: + return "conference"; + case HYBBX_AREA_PROXYMAIL: + return "proxymail"; + case HYBBX_AREA_PROXYCHAT: + return "proxychat"; + default: + return "main"; + } +} + +hybbx_session_area_t hybbx_session_area_parse(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return HYBBX_AREA_MAIN; + } + + if (session_str_ieq(name, "main")) { + return HYBBX_AREA_MAIN; + } + + if (session_str_ieq(name, "mail")) { + return HYBBX_AREA_MAIL; + } + + if (session_str_ieq(name, "chat")) { + return HYBBX_AREA_CHAT; + } + + if (session_str_ieq(name, "conference")) { + return HYBBX_AREA_CONFERENCE; + } + + if (session_str_ieq(name, "proxymail")) { + return HYBBX_AREA_PROXYMAIL; + } + + if (session_str_ieq(name, "proxychat")) { + return HYBBX_AREA_PROXYCHAT; + } + + return HYBBX_AREA_MAIN; +} + +hybbx_result_t hybbx_session_enter_area(hybbx_session_t *session, + hybbx_session_area_t area) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + return session_area_push(core, area); +} + +hybbx_result_t hybbx_session_leave_area(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_session_area_t leaving; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area_depth <= 1) { + return HYBBX_OK; + } + + leaving = core->area_stack[core->area_depth - 1]; + session_area_clear_state(core, leaving); + core->area_depth--; + session_area_sync(core); + return HYBBX_OK; +} + +hybbx_result_t hybbx_session_go_main(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + while (core->area_depth > 1) { + hybbx_session_area_t leaving = core->area_stack[core->area_depth - 1]; + + session_area_clear_state(core, leaving); + core->area_depth--; + } + + core->area_stack[0] = HYBBX_AREA_MAIN; + core->area_depth = 1; + core->area = HYBBX_AREA_MAIN; + return HYBBX_OK; +} + +hybbx_result_t hybbx_session_join_chat_channel(hybbx_session_t *session, + unsigned channel_index) +{ + hybbx_session_core_t *core; + const hybbx_chat_config_t *chat; + const char *name; + + if (session == NULL || channel_index == 0) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || core->service == NULL) { + return HYBBX_ERR_INVALID; + } + + chat = hybbx_service_get_chat(core->service); + name = hybbx_chat_channel_name(chat, channel_index); + if (name == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (core->area != HYBBX_AREA_CHAT) { + hybbx_result_t push_rc = session_area_push(core, HYBBX_AREA_CHAT); + + if (push_rc != HYBBX_OK) { + return push_rc; + } + } + + core->chat_channel = channel_index; + + if (!core->chat_max_notice_shown) { + char notice[96]; + + snprintf(notice, sizeof(notice), "Max %u chars per message.", + chat->message_max); + hybbx_session_write_line(session, notice); + core->chat_max_notice_shown = 1; + } + + return HYBBX_OK; +} + +unsigned hybbx_session_chat_channel(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || core->area != HYBBX_AREA_CHAT) { + return 0; + } + + return core->chat_channel; +} + +hybbx_service_t *hybbx_session_service(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return NULL; + } + + return core->service; +} + +static int session_str_ieq_local(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +int hybbx_session_conference_may_invite(hybbx_session_t *session, + const char *target) +{ + hybbx_session_core_t *core; + size_t s; + size_t k; + time_t now; + unsigned count; + + if (session == NULL || target == NULL || target[0] == '\0') { + return 0; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + now = time(NULL); + for (s = 0; s < sizeof(core->conference_invite_rates) / + sizeof(core->conference_invite_rates[0]); s++) { + if (!session_str_ieq_local(core->conference_invite_rates[s].target, + target)) { + continue; + } + + count = 0; + for (k = 0; k < HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET; k++) { + time_t sent = core->conference_invite_rates[s].sent_at[k]; + + if (sent != 0 && + (now - sent) < (time_t)HYBBX_CONFERENCE_INVITE_WINDOW_SEC) { + count++; + } + } + + return count < HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET; + } + + return 1; +} + +void hybbx_session_conference_invite_sent(hybbx_session_t *session, + const char *target) +{ + hybbx_session_core_t *core; + size_t s; + size_t slot = (size_t)-1; + size_t empty = (size_t)-1; + size_t k; + time_t now; + + if (session == NULL || target == NULL || target[0] == '\0') { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + now = time(NULL); + + for (s = 0; s < sizeof(core->conference_invite_rates) / + sizeof(core->conference_invite_rates[0]); s++) { + if (core->conference_invite_rates[s].target[0] == '\0') { + if (empty == (size_t)-1) { + empty = s; + } + continue; + } + + if (session_str_ieq_local(core->conference_invite_rates[s].target, + target)) { + slot = s; + break; + } + } + + if (slot == (size_t)-1) { + slot = empty; + } + + if (slot == (size_t)-1) { + slot = 0; + } + + if (core->conference_invite_rates[slot].target[0] == '\0') { + hybbx_strlcpy(core->conference_invite_rates[slot].target, target, + sizeof(core->conference_invite_rates[slot].target)); + } + + for (k = 0; k < HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET - 1u; k++) { + core->conference_invite_rates[slot].sent_at[k] = + core->conference_invite_rates[slot].sent_at[k + 1]; + } + + core->conference_invite_rates[slot].sent_at[ + HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET - 1u] = now; +} + +void hybbx_session_set_conference_invite(hybbx_session_t *session, + const char *from_username, + const char *topic) +{ + hybbx_session_core_t *core; + + if (session == NULL || from_username == NULL || topic == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_invite_pending = 1; + core->conference_invite_deadline = 0; + hybbx_strlcpy(core->conference_invite_from, from_username, + sizeof(core->conference_invite_from)); + hybbx_strlcpy(core->conference_invite_topic, topic, + sizeof(core->conference_invite_topic)); +} + +void hybbx_session_set_conference_invite_deadline(hybbx_session_t *session, + time_t deadline) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_invite_deadline = deadline; +} + +time_t hybbx_session_conference_invite_deadline(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->conference_invite_pending) { + return 0; + } + + return core->conference_invite_deadline; +} + +void hybbx_session_clear_conference_invite(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_invite_pending = 0; + core->conference_invite_deadline = 0; + core->conference_invite_from[0] = '\0'; + core->conference_invite_topic[0] = '\0'; +} + +int hybbx_conference_invite_pending(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->conference_invite_pending != 0; +} + +const char *hybbx_session_conference_invite_from(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->conference_invite_pending) { + return NULL; + } + + return core->conference_invite_from; +} + +const char *hybbx_session_conference_invite_topic(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->conference_invite_pending) { + return NULL; + } + + return core->conference_invite_topic; +} + +hybbx_result_t hybbx_session_join_conference(hybbx_session_t *session, + const char *topic, + const char *partner_username) +{ + hybbx_session_core_t *core; + const hybbx_chat_config_t *chat; + + if (session == NULL || topic == NULL || partner_username == NULL || + topic[0] == '\0' || partner_username[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || core->service == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area != HYBBX_AREA_CONFERENCE) { + hybbx_result_t push_rc = session_area_push(core, HYBBX_AREA_CONFERENCE); + + if (push_rc != HYBBX_OK) { + return push_rc; + } + } + + hybbx_strlcpy(core->conference_topic, topic, sizeof(core->conference_topic)); + hybbx_strlcpy(core->conference_partner, partner_username, + sizeof(core->conference_partner)); + + chat = hybbx_service_get_chat(core->service); + if (chat != NULL && !core->chat_max_notice_shown) { + char notice[96]; + + snprintf(notice, sizeof(notice), "Max %u chars per message.", + chat->message_max); + hybbx_session_write_line(session, notice); + core->chat_max_notice_shown = 1; + } + + return HYBBX_OK; +} + +void hybbx_session_clear_conference(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->conference_topic[0] = '\0'; + core->conference_partner[0] = '\0'; +} + +const char *hybbx_session_conference_partner(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return NULL; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || core->conference_partner[0] == '\0') { + return NULL; + } + + return core->conference_partner; +} + +int hybbx_session_mail_composing(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->mail_composing; +} + +hybbx_result_t hybbx_session_mail_compose_start(hybbx_session_t *session, + const char *to_user, + const char *subject) +{ + hybbx_session_core_t *core; + const hybbx_mail_config_t *mail; + hybbx_user_record_t recipient; + hybbx_storage_t *storage; + size_t subject_len; + + if (session == NULL || to_user == NULL || subject == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(core->service); + subject_len = strlen(subject); + if (mail != NULL && subject_len > mail->subject_max) { + return HYBBX_ERR_INVALID; + } + + storage = hybbx_service_get_storage(core->service); + if (storage != NULL && + hybbx_storage_resolve_user(storage, to_user, &recipient) == HYBBX_OK) { + hybbx_strlcpy(core->mail_compose_to, recipient.username, + sizeof(core->mail_compose_to)); + } else { + hybbx_strlcpy(core->mail_compose_to, to_user, + sizeof(core->mail_compose_to)); + hybbx_username_normalize(core->mail_compose_to); + } + hybbx_strlcpy(core->mail_compose_subject, subject, + sizeof(core->mail_compose_subject)); + core->mail_compose_body[0] = '\0'; + core->mail_compose_body_len = 0; + core->mail_composing = 1; + + if (core->area != HYBBX_AREA_MAIL) { + return session_area_push(core, HYBBX_AREA_MAIL); + } + + return HYBBX_OK; +} + +void hybbx_session_mail_compose_cancel(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->mail_composing = 0; + core->mail_compose_body[0] = '\0'; + core->mail_compose_body_len = 0; + core->mail_compose_to[0] = '\0'; + core->mail_compose_subject[0] = '\0'; +} + +const char *hybbx_session_mail_compose_body(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->mail_composing) { + return ""; + } + + return core->mail_compose_body; +} + +const char *hybbx_session_mail_compose_to(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->mail_composing) { + return ""; + } + + return core->mail_compose_to; +} + +const char *hybbx_session_mail_compose_subject(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->mail_composing) { + return ""; + } + + return core->mail_compose_subject; +} + +hybbx_result_t hybbx_session_enter_mail(hybbx_session_t *session) +{ + return hybbx_session_enter_area(session, HYBBX_AREA_MAIL); +} + +hybbx_result_t hybbx_session_enter_chat(hybbx_session_t *session) +{ + return hybbx_session_enter_area(session, HYBBX_AREA_CHAT); +} + +hybbx_result_t hybbx_session_enter_proxymail(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area != HYBBX_AREA_MAIL && + core->area != HYBBX_AREA_PROXYMAIL) { + rc = session_area_push(core, HYBBX_AREA_MAIL); + if (rc != HYBBX_OK) { + return rc; + } + } + + return session_area_push(core, HYBBX_AREA_PROXYMAIL); +} + +hybbx_result_t hybbx_session_enter_proxychat(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + if (core->area != HYBBX_AREA_CHAT && + core->area != HYBBX_AREA_PROXYCHAT) { + rc = session_area_push(core, HYBBX_AREA_CHAT); + if (rc != HYBBX_OK) { + return rc; + } + } + + return session_area_push(core, HYBBX_AREA_PROXYCHAT); +} + +int hybbx_session_proxymail_composing(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->proxymail_composing; +} + +hybbx_result_t hybbx_session_proxymail_compose_start(hybbx_session_t *session, + const char *to_address, + const char *subject) +{ + hybbx_session_core_t *core; + const hybbx_mail_config_t *mail; + size_t subject_len; + + if (session == NULL || to_address == NULL || subject == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_proxymail_parse_address(to_address, NULL, 0, NULL, 0)) { + return HYBBX_ERR_INVALID; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return HYBBX_ERR_INVALID; + } + + mail = hybbx_service_get_mail(core->service); + subject_len = strlen(subject); + if (mail != NULL && subject_len > mail->subject_max) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(core->proxymail_compose_to, to_address, + sizeof(core->proxymail_compose_to)); + hybbx_strlcpy(core->proxymail_compose_subject, subject, + sizeof(core->proxymail_compose_subject)); + core->proxymail_compose_body[0] = '\0'; + core->proxymail_compose_body_len = 0; + core->proxymail_composing = 1; + + return hybbx_session_enter_proxymail(session); +} + +void hybbx_session_proxymail_compose_cancel(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->proxymail_composing = 0; + core->proxymail_compose_body[0] = '\0'; + core->proxymail_compose_body_len = 0; + core->proxymail_compose_to[0] = '\0'; + core->proxymail_compose_subject[0] = '\0'; +} + +const char *hybbx_session_proxymail_compose_body(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->proxymail_composing) { + return ""; + } + + return core->proxymail_compose_body; +} + +const char *hybbx_session_proxymail_compose_to(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->proxymail_composing) { + return ""; + } + + return core->proxymail_compose_to; +} + +const char *hybbx_session_proxymail_compose_subject( + const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return ""; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->proxymail_composing) { + return ""; + } + + return core->proxymail_compose_subject; +} + +time_t hybbx_session_connected_at(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->record.connected_at; +} + +int hybbx_session_bandwidth_paused(const hybbx_session_t *session) +{ + const hybbx_session_core_t *core; + + if (session == NULL) { + return 0; + } + + core = (const hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return 0; + } + + return core->bandwidth_paused; +} + +void hybbx_session_set_bandwidth_paused(hybbx_session_t *session, int paused) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL) { + return; + } + + core->bandwidth_paused = paused ? 1 : 0; +} + +void hybbx_session_disconnect_bandwidth(hybbx_session_t *session) +{ + hybbx_session_core_t *core; + + if (session == NULL) { + return; + } + + core = (hybbx_session_core_t *)session->core_data; + if (core == NULL || !core->logged_in) { + return; + } + + core->bandwidth_paused = 0; + (void)hybbx_traffic_emit(session, &core->out_col, + HYBBX_BANDWIDTH_DISCONNECT_MSG, + strlen(HYBBX_BANDWIDTH_DISCONNECT_MSG)); + (void)hybbx_traffic_emit(session, &core->out_col, "\n", 1); + core->bandwidth_disconnect = 1; +} diff --git a/src/core/storage.c b/src/core/storage.c new file mode 100644 index 0000000..8217dd9 --- /dev/null +++ b/src/core/storage.c @@ -0,0 +1,378 @@ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/config.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "storage_private.h" + +#include +#include +#include + +#define HYBBX_SQL_DEFAULT_USER_DB "users.db" +#define HYBBX_SQL_DEFAULT_MAIL_DB "mail.db" + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +void hybbx_storage_sql_config_defaults(hybbx_storage_sql_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->user_db[0] = '\0'; + cfg->mail_db[0] = '\0'; + cfg->backup_interval_sec = HYBBX_STORAGE_BACKUP_INTERVAL_DEFAULT_SEC; + cfg->backup_path[0] = '\0'; +} + +void hybbx_storage_sql_config_apply(hybbx_storage_sql_config_t *cfg, + const hybbx_config_t *config, + const char *storage_path) +{ + const char *user_db; + const char *mail_db; + const char *backup_path; + + hybbx_storage_sql_config_defaults(cfg); + if (storage_path == NULL || storage_path[0] == '\0') { + return; + } + + user_db = HYBBX_SQL_DEFAULT_USER_DB; + mail_db = HYBBX_SQL_DEFAULT_MAIL_DB; + backup_path = ""; + + if (config != NULL) { + user_db = hybbx_config_get(config, "storage", "user_db", + HYBBX_SQL_DEFAULT_USER_DB); + mail_db = hybbx_config_get(config, "storage", "mail_db", + HYBBX_SQL_DEFAULT_MAIL_DB); + backup_path = hybbx_config_get(config, "storage", "backup_path", ""); + cfg->backup_interval_sec = hybbx_config_get_uint( + config, "storage", "backup_interval", + HYBBX_STORAGE_BACKUP_INTERVAL_DEFAULT_SEC, 60u, 86400u); + } + + (void)hybbx_path_join(cfg->user_db, sizeof(cfg->user_db), storage_path, + user_db); + (void)hybbx_path_join(cfg->mail_db, sizeof(cfg->mail_db), storage_path, + mail_db); + + if (backup_path != NULL && backup_path[0] != '\0') { + char resolved[HYBBX_PATH_MAX]; + + if (hybbx_path_resolve(resolved, sizeof(resolved), backup_path) == + HYBBX_OK) { + hybbx_strlcpy(cfg->backup_path, resolved, sizeof(cfg->backup_path)); + } else { + (void)hybbx_path_join(cfg->backup_path, sizeof(cfg->backup_path), + storage_path, backup_path); + } + } +} + +const hybbx_storage_sql_config_t *hybbx_storage_sql_config( + const hybbx_storage_t *storage) +{ + if (storage == NULL) { + return NULL; + } + + return &storage->sql_cfg; +} + +void hybbx_storage_backup_tick(hybbx_storage_t *storage) +{ + hybbx_storage_sql_backup_tick(storage); +} + +static void storage_dispatch_sqlite(hybbx_storage_t *storage, + const hybbx_storage_sql_config_t *sql_cfg) +{ + hybbx_storage_sql_config_defaults(&storage->sql_cfg); + if (sql_cfg != NULL) { + storage->sql_cfg = *sql_cfg; + } else if (storage->path != NULL) { + char user_db[HYBBX_PATH_MAX]; + char mail_db[HYBBX_PATH_MAX]; + + (void)hybbx_path_join(user_db, sizeof(user_db), storage->path, + HYBBX_SQL_DEFAULT_USER_DB); + (void)hybbx_path_join(mail_db, sizeof(mail_db), storage->path, + HYBBX_SQL_DEFAULT_MAIL_DB); + hybbx_strlcpy(storage->sql_cfg.user_db, user_db, + sizeof(storage->sql_cfg.user_db)); + hybbx_strlcpy(storage->sql_cfg.mail_db, mail_db, + sizeof(storage->sql_cfg.mail_db)); + } +} + +hybbx_storage_t *hybbx_storage_open(const hybbx_storage_options_t *options) +{ + hybbx_storage_t *storage; + hybbx_result_t rc; + + if (options == NULL || options->path == NULL) { + return NULL; + } + + storage = calloc(1, sizeof(*storage)); + if (storage == NULL) { + return NULL; + } + + storage->backend = options->backend; + storage->path = hybbx_strdup(options->path); + if (storage->path == NULL) { + free(storage); + return NULL; + } + + if (options->guest_prefix != NULL && options->guest_prefix[0] != '\0') { + hybbx_strlcpy(storage->guest_prefix, options->guest_prefix, + sizeof(storage->guest_prefix)); + } else { + hybbx_strlcpy(storage->guest_prefix, HYBBX_AUTH_DEFAULT_GUEST_PREFIX, + sizeof(storage->guest_prefix)); + } + + if (storage->backend == HYBBX_STORAGE_SQLITE) { + storage_dispatch_sqlite(storage, options->sql_cfg); + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + rc = hybbx_storage_flatfile_open(storage); + break; + case HYBBX_STORAGE_SQLITE: + case HYBBX_STORAGE_MYSQL: + case HYBBX_STORAGE_MARIADB: + rc = hybbx_storage_sql_open(storage); + break; + default: + rc = HYBBX_ERR_UNSUPPORTED; + break; + } + + if (rc != HYBBX_OK) { + hybbx_storage_close(storage); + return NULL; + } + + return storage; +} + +void hybbx_storage_close(hybbx_storage_t *storage) +{ + if (storage == NULL) { + return; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + hybbx_storage_flatfile_close(storage); + break; + case HYBBX_STORAGE_SQLITE: + case HYBBX_STORAGE_MYSQL: + case HYBBX_STORAGE_MARIADB: + hybbx_storage_sql_close(storage); + break; + default: + break; + } + + free(storage->path); + free(storage); +} + +hybbx_storage_backend_kind_t hybbx_storage_backend(const hybbx_storage_t *storage) +{ + if (storage == NULL) { + return HYBBX_STORAGE_FLATFILE; + } + return storage->backend; +} + +const char *hybbx_storage_root_path(const hybbx_storage_t *storage) +{ + if (storage == NULL || storage->path == NULL || storage->path[0] == '\0') { + return NULL; + } + return storage->path; +} + +hybbx_result_t hybbx_storage_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + if (storage == NULL || reg == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_register_user(storage, reg, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_register_user(storage, reg, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + if (storage == NULL || username == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_find_user(storage, username, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_find_user(storage, username, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + if (storage == NULL || name == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_resolve_user(storage, name, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_resolve_user(storage, name, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + if (storage == NULL || count == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_count_level(storage, level, count); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_count_level(storage, level, count); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + if (storage == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_foreach_user(storage, fn, ctx); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_foreach_user(storage, fn, ctx); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + if (storage == NULL || user == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_session_begin(storage, user, transport, out); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_session_begin(storage, user, transport, out); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + if (storage == NULL || session_id == 0) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_session_end(storage, session_id); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_session_end(storage, session_id); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + if (storage == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_update_user(storage, user); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_update_user(storage, user); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} + +hybbx_result_t hybbx_storage_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + if (storage == NULL || user_id == 0) { + return HYBBX_ERR_INVALID; + } + + switch (storage->backend) { + case HYBBX_STORAGE_FLATFILE: + return hybbx_storage_flatfile_delete_user(storage, user_id); + case HYBBX_STORAGE_SQLITE: + return hybbx_storage_sql_delete_user(storage, user_id); + default: + return HYBBX_ERR_UNSUPPORTED; + } +} diff --git a/src/core/storage_flatfile.c b/src/core/storage_flatfile.c new file mode 100644 index 0000000..4915da6 --- /dev/null +++ b/src/core/storage_flatfile.c @@ -0,0 +1,1507 @@ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/password.h" +#include "hybbx/security.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "storage_private.h" + +#include +#include +#include +#include +#include +#include +#include + +struct flatfile_state { + char users_dir[HYBBX_PATH_MAX]; + char legacy_users_path[HYBBX_PATH_MAX]; + char sessions_path[HYBBX_PATH_MAX]; + char session_next_path[HYBBX_PATH_MAX]; + char user_next_path[HYBBX_PATH_MAX]; +}; + +typedef struct user_shard { + hybbx_user_record_t users[HYBBX_USERS_PER_FILE]; + size_t count; +} user_shard_t; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t read_counter(const char *path, uint64_t *value) +{ + FILE *fp; + unsigned long long n = 0; + + fp = fopen(path, "r"); + if (fp == NULL) { + *value = 0; + return HYBBX_OK; + } + + if (fscanf(fp, "%llu", &n) != 1) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + *value = (uint64_t)n; + return HYBBX_OK; +} + +static hybbx_result_t write_counter(const char *path, uint64_t value) +{ + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%llu\n", (unsigned long long)value); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t bump_counter(const char *path, uint64_t *value) +{ + hybbx_result_t rc; + + rc = read_counter(path, value); + if (rc != HYBBX_OK) { + return rc; + } + + (*value)++; + return write_counter(path, *value); +} + +static hybbx_result_t append_line(const char *path, const char *line) +{ + FILE *fp; + + fp = fopen(path, "a"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%s\n", line); + fclose(fp); + return HYBBX_OK; +} + +#define HYBBX_USER_NICK_SUFFIX ".nick" + +static hybbx_result_t user_nickname_path(const struct flatfile_state *state, + const char *username, + char *out, size_t out_len) +{ + char name[HYBBX_USER_NAME_MAX + 8]; + int n; + + if (state == NULL || username == NULL || username[0] == '\0' || + out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + n = snprintf(name, sizeof(name), "%s%s", username, HYBBX_USER_NICK_SUFFIX); + if (n < 0 || (size_t)n >= sizeof(name)) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, state->users_dir, name); +} + +static hybbx_result_t read_nickname_file(const struct flatfile_state *state, + const char *username, + char *nickname, + size_t nickname_len) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + char line[HYBBX_USER_NICKNAME_MAX]; + char *nl; + hybbx_result_t rc; + + if (nickname == NULL || nickname_len == 0) { + return HYBBX_ERR_INVALID; + } + + nickname[0] = '\0'; + + rc = user_nickname_path(state, username, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + if (fgets(line, sizeof(line), fp) == NULL) { + fclose(fp); + return HYBBX_ERR_IO; + } + + fclose(fp); + + nl = strchr(line, '\n'); + if (nl != NULL) { + *nl = '\0'; + } + nl = strchr(line, '\r'); + if (nl != NULL) { + *nl = '\0'; + } + + if (line[0] == '\0') { + return HYBBX_ERR_NOT_FOUND; + } + + hybbx_strlcpy(nickname, line, nickname_len); + return HYBBX_OK; +} + +static hybbx_result_t write_nickname_file(const struct flatfile_state *state, + const char *username, + const char *nickname) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + hybbx_result_t rc; + + if (state == NULL || username == NULL || username[0] == '\0' || + nickname == NULL || nickname[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = user_nickname_path(state, username, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "%s\n", nickname); + if (fclose(fp) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void remove_nickname_file(const struct flatfile_state *state, + const char *username) +{ + char path[HYBBX_PATH_MAX]; + + if (state == NULL || username == NULL || username[0] == '\0') { + return; + } + + if (user_nickname_path(state, username, path, sizeof(path)) == HYBBX_OK) { + (void)remove(path); + } +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int file_exists(const char *path) +{ + struct stat st; + + return path != NULL && stat(path, &st) == 0 && S_ISREG(st.st_mode); +} + +static hybbx_result_t user_shard_path(const struct flatfile_state *state, + unsigned shard_index, + char *out, size_t out_len) +{ + char name[32]; + int n; + + if (state == NULL || out == NULL || out_len == 0 || shard_index == 0) { + return HYBBX_ERR_INVALID; + } + + if (shard_index == 1) { + n = snprintf(name, sizeof(name), "users.ini"); + } else { + n = snprintf(name, sizeof(name), "users%u.ini", shard_index); + } + + if (n < 0 || (size_t)n >= sizeof(name)) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, state->users_dir, name); +} + +static unsigned last_user_shard_index(const struct flatfile_state *state) +{ + char path[HYBBX_PATH_MAX]; + unsigned index; + + for (index = 1; index < 10000u; index++) { + if (user_shard_path(state, index, path, sizeof(path)) != HYBBX_OK) { + break; + } + if (!file_exists(path)) { + break; + } + } + + if (index == 1) { + return 0; + } + + return index - 1; +} + +static int parse_legacy_user_line(const char *line, hybbx_user_record_t *out) +{ + unsigned long long id = 0; + unsigned long long active = 0; + long long created = 0; + char username[HYBBX_USER_NAME_MAX]; + char level_name[32]; + char full_name[HYBBX_USER_FULL_NAME_MAX]; + char country[HYBBX_USER_COUNTRY_MAX]; + char location[HYBBX_USER_LOCATION_MAX]; + char email[HYBBX_USER_EMAIL_MAX]; + char password[HYBBX_USER_PASSWORD_MAX]; + int fields; + + if (line == NULL || out == NULL) { + return 0; + } + + full_name[0] = '\0'; + country[0] = '\0'; + location[0] = '\0'; + email[0] = '\0'; + password[0] = '\0'; + + fields = sscanf(line, + "%llu|%63[^|]|%31[^|]|%llu|%lld|" + "%127[^|]|%63[^|]|%127[^|]|%127[^|]|%95[^|]", + &id, username, level_name, &active, &created, + full_name, country, location, email, password); + if (fields < 5) { + return 0; + } + + memset(out, 0, sizeof(*out)); + out->id = (uint64_t)id; + hybbx_strlcpy(out->username, username, sizeof(out->username)); + out->level = hybbx_user_level_parse(level_name); + out->active = (int)active; + out->created_at = (time_t)created; + + if (fields >= 9) { + hybbx_strlcpy(out->full_name, full_name, sizeof(out->full_name)); + hybbx_strlcpy(out->country, country, sizeof(out->country)); + hybbx_strlcpy(out->location, location, sizeof(out->location)); + hybbx_strlcpy(out->email, email, sizeof(out->email)); + } + + if (fields >= 10) { + hybbx_strlcpy(out->password, password, sizeof(out->password)); + } + + return 1; +} + +static int parse_user_section(const hybbx_config_t *cfg, const char *section, + hybbx_user_record_t *out) +{ + const char *value; + unsigned long long n; + + if (cfg == NULL || section == NULL || out == NULL) { + return 0; + } + + if (strncmp(section, HYBBX_USER_INI_SECTION_PREFIX, + strlen(HYBBX_USER_INI_SECTION_PREFIX)) != 0) { + return 0; + } + + memset(out, 0, sizeof(*out)); + + value = hybbx_config_get(cfg, section, "id", NULL); + if (value == NULL || value[0] == '\0') { + return 0; + } + n = strtoull(value, NULL, 10); + if (n == 0) { + return 0; + } + out->id = (uint64_t)n; + + value = hybbx_config_get(cfg, section, "username", NULL); + if (value == NULL || value[0] == '\0') { + return 0; + } + hybbx_strlcpy(out->username, value, sizeof(out->username)); + hybbx_username_normalize(out->username); + + value = hybbx_config_get(cfg, section, "nickname", NULL); + if (value != NULL) { + hybbx_strlcpy(out->nickname, value, sizeof(out->nickname)); + } + + value = hybbx_config_get(cfg, section, "level", "user"); + out->level = hybbx_user_level_parse(value); + out->active = hybbx_config_get_bool(cfg, section, "active", 0); + + value = hybbx_config_get(cfg, section, "created", NULL); + if (value != NULL && value[0] != '\0') { + out->created_at = (time_t)strtoll(value, NULL, 10); + } + + value = hybbx_config_get(cfg, section, "fullname", NULL); + if (value != NULL) { + hybbx_strlcpy(out->full_name, value, sizeof(out->full_name)); + } + + value = hybbx_config_get(cfg, section, "country", NULL); + if (value != NULL) { + hybbx_strlcpy(out->country, value, sizeof(out->country)); + } + + value = hybbx_config_get(cfg, section, "location", NULL); + if (value != NULL) { + hybbx_strlcpy(out->location, value, sizeof(out->location)); + } + + value = hybbx_config_get(cfg, section, "email", NULL); + if (value != NULL) { + hybbx_strlcpy(out->email, value, sizeof(out->email)); + } + + value = hybbx_config_get(cfg, section, "password", NULL); + if (value != NULL) { + hybbx_strlcpy(out->password, value, sizeof(out->password)); + } + + value = hybbx_config_get(cfg, section, "last_login", NULL); + if (value != NULL && value[0] != '\0') { + out->last_login_at = (time_t)strtoll(value, NULL, 10); + } + + return 1; +} + +typedef struct section_list_ctx { + char names[HYBBX_USERS_PER_FILE][HYBBX_CONFIG_SECTION_MAX]; + size_t count; +} section_list_ctx_t; + +static void section_list_collect(const char *section, const char *key, + const char *value, void *ctx) +{ + section_list_ctx_t *list = (section_list_ctx_t *)ctx; + size_t i; + + (void)key; + (void)value; + + if (section == NULL || list == NULL) { + return; + } + + if (strncmp(section, HYBBX_USER_INI_SECTION_PREFIX, + strlen(HYBBX_USER_INI_SECTION_PREFIX)) != 0) { + return; + } + + for (i = 0; i < list->count; i++) { + if (strcmp(list->names[i], section) == 0) { + return; + } + } + + if (list->count >= HYBBX_USERS_PER_FILE) { + return; + } + + hybbx_strlcpy(list->names[list->count], section, + sizeof(list->names[list->count])); + list->count++; +} + +static hybbx_result_t load_user_shard(const char *path, user_shard_t *shard) +{ + hybbx_config_t cfg; + section_list_ctx_t sections; + size_t i; + hybbx_result_t rc; + + if (path == NULL || shard == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(shard, 0, sizeof(*shard)); + memset(§ions, 0, sizeof(sections)); + + rc = hybbx_config_load(&cfg, path); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_config_foreach(&cfg, section_list_collect, §ions); + + for (i = 0; i < sections.count; i++) { + hybbx_user_record_t user; + + if (!parse_user_section(&cfg, sections.names[i], &user)) { + continue; + } + + if (shard->count >= HYBBX_USERS_PER_FILE) { + hybbx_config_free(&cfg); + return HYBBX_ERR_IO; + } + + shard->users[shard->count++] = user; + } + + hybbx_config_free(&cfg); + return HYBBX_OK; +} + +static hybbx_result_t save_user_shard(const struct flatfile_state *state, + unsigned shard_index, + const user_shard_t *shard) +{ + char path[HYBBX_PATH_MAX]; + FILE *fp; + size_t i; + hybbx_result_t rc; + + if (state == NULL || shard == NULL || shard->count > HYBBX_USERS_PER_FILE) { + return HYBBX_ERR_INVALID; + } + + rc = user_shard_path(state, shard_index, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "; HyBBX user file (max %u users)\n", + (unsigned)HYBBX_USERS_PER_FILE); + fprintf(fp, "; shard %u\n\n", shard_index); + + for (i = 0; i < shard->count; i++) { + const hybbx_user_record_t *user = &shard->users[i]; + + fprintf(fp, "[user.%llu]\n", (unsigned long long)user->id); + fprintf(fp, "id = %llu\n", (unsigned long long)user->id); + fprintf(fp, "username = %s\n", user->username); + fprintf(fp, "nickname = %s\n", user->nickname); + fprintf(fp, "level = %s\n", hybbx_user_level_name(user->level)); + fprintf(fp, "active = %s\n", hybbx_bool_to_string(user->active)); + fprintf(fp, "created = %lld\n", (long long)user->created_at); + fprintf(fp, "fullname = %s\n", user->full_name); + fprintf(fp, "country = %s\n", user->country); + fprintf(fp, "location = %s\n", user->location); + fprintf(fp, "email = %s\n", user->email); + fprintf(fp, "password = %s\n", user->password); + fprintf(fp, "last_login = %lld\n", (long long)user->last_login_at); + + if (user->nickname[0] != '\0') { + (void)write_nickname_file(state, user->username, user->nickname); + } + } + + if (fclose(fp) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t find_user_shard(const struct flatfile_state *state, + uint64_t user_id, + unsigned *shard_index, + size_t *slot_index) +{ + unsigned shard; + unsigned last; + char path[HYBBX_PATH_MAX]; + user_shard_t loaded; + size_t i; + hybbx_result_t rc; + + if (state == NULL || user_id == 0 || shard_index == NULL || + slot_index == NULL) { + return HYBBX_ERR_INVALID; + } + + last = last_user_shard_index(state); + + for (shard = 1; shard <= last; shard++) { + if (user_shard_path(state, shard, path, sizeof(path)) != HYBBX_OK) { + continue; + } + if (!file_exists(path)) { + continue; + } + + rc = load_user_shard(path, &loaded); + if (rc != HYBBX_OK) { + return rc; + } + + for (i = 0; i < loaded.count; i++) { + if (loaded.users[i].id == user_id) { + *shard_index = shard; + *slot_index = i; + return HYBBX_OK; + } + } + } + + return HYBBX_ERR_NOT_FOUND; +} + +static hybbx_result_t append_user_record(struct flatfile_state *state, + const hybbx_user_record_t *user) +{ + unsigned shard; + unsigned last; + char path[HYBBX_PATH_MAX]; + user_shard_t loaded; + hybbx_result_t rc; + + if (state == NULL || user == NULL) { + return HYBBX_ERR_INVALID; + } + + last = last_user_shard_index(state); + if (last == 0) { + shard = 1; + memset(&loaded, 0, sizeof(loaded)); + } else { + rc = user_shard_path(state, last, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &loaded); + if (rc != HYBBX_OK) { + return rc; + } + + if (loaded.count >= HYBBX_USERS_PER_FILE) { + shard = last + 1; + memset(&loaded, 0, sizeof(loaded)); + } else { + shard = last; + } + } + + if (loaded.count >= HYBBX_USERS_PER_FILE) { + return HYBBX_ERR_BUSY; + } + + loaded.users[loaded.count++] = *user; + return save_user_shard(state, shard, &loaded); +} + +static hybbx_result_t ensure_default_sysop(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + size_t sysop_count = 0; + hybbx_user_record_t sysop; + char plain_password[HYBBX_PASSWORD_MAX_LEN + 1]; + uint64_t user_id; + time_t now = time(NULL); + hybbx_result_t rc; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_flatfile_count_level(storage, HYBBX_LEVEL_SYSOP, + &sysop_count); + if (rc != HYBBX_OK) { + return rc; + } + + if (sysop_count > 0) { + return HYBBX_OK; + } + + rc = bump_counter(state->user_next_path, &user_id); + if (rc != HYBBX_OK) { + return rc; + } + + memset(&sysop, 0, sizeof(sysop)); + sysop.id = user_id; + hybbx_strlcpy(sysop.username, "sysop", sizeof(sysop.username)); + hybbx_strlcpy(sysop.nickname, HYBBX_DEFAULT_SYSOP_USERNAME, + sizeof(sysop.nickname)); + sysop.level = HYBBX_LEVEL_SYSOP; + sysop.active = 1; + sysop.created_at = now; + hybbx_strlcpy(sysop.full_name, "System Operator", sizeof(sysop.full_name)); + if (hybbx_password_generate_alnum(plain_password, sizeof(plain_password), + HYBBX_SYSOP_INIT_PASSWORD_MIN, + HYBBX_SYSOP_INIT_PASSWORD_MAX) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (hybbx_password_hash(plain_password, + sysop.password, sizeof(sysop.password)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = append_user_record(state, &sysop); + if (rc != HYBBX_OK) { + return rc; + } + + hybbx_log_info("[storage] created default Sysop at %s/users/users.ini (login: %s / %s)", + storage->path, sysop.nickname, plain_password); + hybbx_security_log_write("sysop_created user=%s", sysop.nickname); + return HYBBX_OK; +} + +static hybbx_result_t foreach_user(struct flatfile_state *state, + hybbx_result_t (*fn)(const hybbx_user_record_t *user, + void *ctx), + void *ctx) +{ + unsigned shard; + unsigned last; + char path[HYBBX_PATH_MAX]; + user_shard_t loaded; + size_t i; + hybbx_result_t rc; + + if (state == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + last = last_user_shard_index(state); + for (shard = 1; shard <= last; shard++) { + rc = user_shard_path(state, shard, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &loaded); + if (rc != HYBBX_OK) { + return rc; + } + + for (i = 0; i < loaded.count; i++) { + rc = fn(&loaded.users[i], ctx); + if (rc != HYBBX_OK) { + return rc; + } + } + } + + return HYBBX_OK; +} + +typedef struct find_user_ctx { + const char *username; + hybbx_user_record_t *out; + int found; +} find_user_ctx_t; + +static hybbx_result_t find_user_cb(const hybbx_user_record_t *user, void *ctx) +{ + find_user_ctx_t *fctx = (find_user_ctx_t *)ctx; + + if (str_ieq(user->username, fctx->username)) { + *fctx->out = *user; + fctx->found = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +typedef struct find_nickname_ctx { + const char *nickname; + hybbx_user_record_t *out; + int found; +} find_nickname_ctx_t; + +static hybbx_result_t find_nickname_cb(const hybbx_user_record_t *user, void *ctx) +{ + find_nickname_ctx_t *fctx = (find_nickname_ctx_t *)ctx; + + if (user->nickname[0] != '\0' && str_ieq(user->nickname, fctx->nickname)) { + *fctx->out = *user; + fctx->found = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +typedef struct identity_taken_ctx { + const char *username; + const char *nickname; + int taken; +} identity_taken_ctx_t; + +static hybbx_result_t identity_taken_cb(const hybbx_user_record_t *user, + void *ctx) +{ + identity_taken_ctx_t *ictx = (identity_taken_ctx_t *)ctx; + + if (str_ieq(user->username, ictx->username) || + str_ieq(user->username, ictx->nickname) || + (user->nickname[0] != '\0' && + (str_ieq(user->nickname, ictx->nickname) || + str_ieq(user->nickname, ictx->username)))) { + ictx->taken = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +typedef struct count_level_ctx { + hybbx_user_level_t level; + size_t count; +} count_level_ctx_t; + +static hybbx_result_t count_level_cb(const hybbx_user_record_t *user, void *ctx) +{ + count_level_ctx_t *cctx = (count_level_ctx_t *)ctx; + + if (user->level == cctx->level) { + cctx->count++; + } + + return HYBBX_OK; +} + +typedef struct migrate_passwords_ctx { + hybbx_storage_t *storage; + hybbx_result_t last_error; +} migrate_passwords_ctx_t; + +static hybbx_result_t migrate_passwords_cb(const hybbx_user_record_t *user, + void *ctx) +{ + migrate_passwords_ctx_t *mctx = (migrate_passwords_ctx_t *)ctx; + hybbx_user_record_t updated; + hybbx_result_t rc; + + if (!hybbx_password_is_plain(user->password)) { + return HYBBX_OK; + } + + updated = *user; + rc = hybbx_password_hash(user->password, updated.password, + sizeof(updated.password)); + if (rc != HYBBX_OK) { + mctx->last_error = rc; + return rc; + } + + rc = hybbx_storage_update_user(mctx->storage, &updated); + if (rc != HYBBX_OK) { + mctx->last_error = rc; + return rc; + } + + hybbx_log_info("[storage] upgraded plain password to sha256 for user %s", + user->username); + return HYBBX_OK; +} + +static hybbx_result_t migrate_legacy_users_dat(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + FILE *fp; + char line[HYBBX_CONFIG_LINE_MAX]; + user_shard_t shard; + unsigned shard_index = 1; + hybbx_result_t rc; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!file_exists(state->legacy_users_path)) { + return HYBBX_OK; + } + + if (last_user_shard_index(state) > 0) { + return HYBBX_OK; + } + + fp = fopen(state->legacy_users_path, "r"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + memset(&shard, 0, sizeof(shard)); + + while (fgets(line, sizeof(line), fp) != NULL) { + hybbx_user_record_t user; + char *nl; + + nl = strchr(line, '\n'); + if (nl != NULL) { + *nl = '\0'; + } + + if (!parse_legacy_user_line(line, &user)) { + continue; + } + + if (shard.count >= HYBBX_USERS_PER_FILE) { + rc = save_user_shard(state, shard_index, &shard); + if (rc != HYBBX_OK) { + fclose(fp); + return rc; + } + shard_index++; + memset(&shard, 0, sizeof(shard)); + } + + shard.users[shard.count++] = user; + } + + fclose(fp); + + if (shard.count > 0) { + rc = save_user_shard(state, shard_index, &shard); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + char backup[HYBBX_PATH_MAX]; + + if (hybbx_path_join(backup, sizeof(backup), storage->path, + "users.dat.migrated") == HYBBX_OK) { + (void)rename(state->legacy_users_path, backup); + } + } + + hybbx_log_info("[storage] migrated legacy users.dat to INI user files"); + return HYBBX_OK; +} + +typedef struct migrate_identities_ctx { + hybbx_storage_t *storage; + struct flatfile_state *state; + hybbx_result_t last_error; +} migrate_identities_ctx_t; + +static hybbx_result_t migrate_identities_cb(const hybbx_user_record_t *user, + void *ctx) +{ + migrate_identities_ctx_t *mctx = (migrate_identities_ctx_t *)ctx; + hybbx_user_record_t updated; + char stored_username[HYBBX_USER_NAME_MAX]; + int changed = 0; + hybbx_result_t rc; + + if (user == NULL || mctx == NULL) { + return HYBBX_OK; + } + + updated = *user; + hybbx_strlcpy(stored_username, updated.username, sizeof(stored_username)); + + hybbx_username_normalize(updated.username); + if (!str_ieq(stored_username, updated.username)) { + changed = 1; + } + + if (updated.nickname[0] == '\0') { + if (read_nickname_file(mctx->state, updated.username, updated.nickname, + sizeof(updated.nickname)) != HYBBX_OK) { + hybbx_nickname_infer(stored_username, updated.nickname, + sizeof(updated.nickname)); + } + changed = 1; + } + + if (!changed) { + (void)write_nickname_file(mctx->state, updated.username, + updated.nickname); + return HYBBX_OK; + } + + rc = hybbx_storage_flatfile_update_user(mctx->storage, &updated); + if (rc != HYBBX_OK) { + mctx->last_error = rc; + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t migrate_user_identities(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + migrate_identities_ctx_t ctx; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.storage = storage; + ctx.state = state; + ctx.last_error = HYBBX_OK; + return foreach_user(state, migrate_identities_cb, &ctx); +} + +static hybbx_result_t migrate_plain_passwords(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + migrate_passwords_ctx_t ctx; + + if (storage == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + ctx.storage = storage; + ctx.last_error = HYBBX_OK; + return foreach_user(state, migrate_passwords_cb, &ctx); +} + +hybbx_result_t hybbx_storage_flatfile_open(hybbx_storage_t *storage) +{ + struct flatfile_state *state; + hybbx_result_t rc; + + if (storage == NULL || storage->path == NULL) { + return HYBBX_ERR_INVALID; + } + + if (mkdir_p(storage->path) != 0) { + hybbx_log_warn("[storage] cannot create data path '%s'", + storage->path); + return HYBBX_ERR_IO; + } + + state = calloc(1, sizeof(*state)); + if (state == NULL) { + return HYBBX_ERR_NOMEM; + } + + rc = hybbx_path_join(state->users_dir, sizeof(state->users_dir), + storage->path, "users"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + if (mkdir_p(state->users_dir) != 0) { + free(state); + return HYBBX_ERR_IO; + } + + rc = hybbx_path_join(state->legacy_users_path, sizeof(state->legacy_users_path), + storage->path, "users.dat"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = hybbx_path_join(state->sessions_path, sizeof(state->sessions_path), + storage->path, "sessions.dat"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = hybbx_path_join(state->session_next_path, sizeof(state->session_next_path), + storage->path, "session.next"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = hybbx_path_join(state->user_next_path, sizeof(state->user_next_path), + storage->path, "user.next"); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + storage->backend_data = state; + + rc = migrate_legacy_users_dat(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + rc = ensure_default_sysop(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + rc = migrate_user_identities(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + rc = migrate_plain_passwords(storage); + if (rc != HYBBX_OK) { + hybbx_storage_flatfile_close(storage); + return rc; + } + + return HYBBX_OK; +} + +void hybbx_storage_flatfile_close(hybbx_storage_t *storage) +{ + if (storage == NULL) { + return; + } + + free(storage->backend_data); + storage->backend_data = NULL; +} + +hybbx_result_t hybbx_storage_flatfile_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + struct flatfile_state *state; + find_user_ctx_t ctx; + hybbx_result_t rc; + + if (storage == NULL || username == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + ctx.username = username; + ctx.out = out; + ctx.found = 0; + + rc = foreach_user(state, find_user_cb, &ctx); + if (rc == HYBBX_ERR_BUSY) { + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_ERR_NOT_FOUND; +} + +hybbx_result_t hybbx_storage_flatfile_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + char normalized[HYBBX_USER_NAME_MAX]; + struct flatfile_state *state; + find_nickname_ctx_t nctx; + hybbx_result_t rc; + + if (storage == NULL || name == NULL || out == NULL || name[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(normalized, name, sizeof(normalized)); + hybbx_username_normalize(normalized); + + rc = hybbx_storage_flatfile_find_user(storage, normalized, out); + if (rc == HYBBX_OK) { + return HYBBX_OK; + } + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + nctx.nickname = name; + nctx.out = out; + nctx.found = 0; + + rc = foreach_user(state, find_nickname_cb, &nctx); + if (rc == HYBBX_ERR_BUSY) { + return HYBBX_OK; + } + if (rc != HYBBX_OK) { + return rc; + } + + return HYBBX_ERR_NOT_FOUND; +} + +hybbx_result_t hybbx_storage_flatfile_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + struct flatfile_state *state; + count_level_ctx_t ctx; + hybbx_result_t rc; + + if (storage == NULL || count == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + ctx.level = level; + ctx.count = 0; + + rc = foreach_user(state, count_level_cb, &ctx); + if (rc != HYBBX_OK) { + return rc; + } + + *count = ctx.count; + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_flatfile_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + struct flatfile_state *state; + + if (storage == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + return foreach_user(state, fn, ctx); +} + +hybbx_result_t hybbx_storage_flatfile_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + struct flatfile_state *state; + hybbx_user_record_t existing; + uint64_t user_id; + time_t now = time(NULL); + hybbx_result_t rc; + + if (storage == NULL || reg == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_registration_valid(reg, storage->guest_prefix)) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_flatfile_find_user(storage, reg->username, &existing); + if (rc == HYBBX_OK) { + return HYBBX_ERR_BUSY; + } + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; + } + + { + identity_taken_ctx_t taken; + + memset(&taken, 0, sizeof(taken)); + taken.username = reg->username; + taken.nickname = reg->nickname; + + rc = foreach_user(state, identity_taken_cb, &taken); + if (rc == HYBBX_ERR_BUSY || taken.taken) { + return HYBBX_ERR_BUSY; + } + if (rc != HYBBX_OK) { + return rc; + } + } + + rc = bump_counter(state->user_next_path, &user_id); + if (rc != HYBBX_OK) { + return rc; + } + + memset(out, 0, sizeof(*out)); + out->id = user_id; + hybbx_strlcpy(out->username, reg->username, sizeof(out->username)); + hybbx_username_normalize(out->username); + hybbx_strlcpy(out->nickname, reg->nickname, sizeof(out->nickname)); + out->level = HYBBX_LEVEL_USER; + out->active = 0; + out->created_at = now; + hybbx_strlcpy(out->full_name, reg->full_name, sizeof(out->full_name)); + hybbx_strlcpy(out->country, reg->country, sizeof(out->country)); + hybbx_strlcpy(out->location, reg->location, sizeof(out->location)); + hybbx_strlcpy(out->email, reg->email, sizeof(out->email)); + + if (reg->password[0] != '\0') { + if (!hybbx_password_plain_valid(reg->password)) { + return HYBBX_ERR_INVALID; + } + rc = hybbx_password_hash(reg->password, out->password, + sizeof(out->password)); + if (rc != HYBBX_OK) { + return rc; + } + } + + return append_user_record(state, out); +} + +hybbx_result_t hybbx_storage_flatfile_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + struct flatfile_state *state; + unsigned shard_index; + size_t slot_index; + char path[HYBBX_PATH_MAX]; + user_shard_t shard; + hybbx_result_t rc; + + if (storage == NULL || user == NULL || user->id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = find_user_shard(state, user->id, &shard_index, &slot_index); + if (rc != HYBBX_OK) { + return rc; + } + + rc = user_shard_path(state, shard_index, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &shard); + if (rc != HYBBX_OK) { + return rc; + } + + if (slot_index >= shard.count) { + return HYBBX_ERR_NOT_FOUND; + } + + shard.users[slot_index] = *user; + hybbx_username_normalize(shard.users[slot_index].username); + return save_user_shard(state, shard_index, &shard); +} + +hybbx_result_t hybbx_storage_flatfile_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + struct flatfile_state *state; + unsigned shard_index; + size_t slot_index; + char path[HYBBX_PATH_MAX]; + user_shard_t shard; + hybbx_result_t rc; + + if (storage == NULL || user_id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = find_user_shard(state, user_id, &shard_index, &slot_index); + if (rc != HYBBX_OK) { + return rc; + } + + rc = user_shard_path(state, shard_index, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + rc = load_user_shard(path, &shard); + if (rc != HYBBX_OK) { + return rc; + } + + if (slot_index >= shard.count) { + return HYBBX_ERR_NOT_FOUND; + } + + remove_nickname_file(state, shard.users[slot_index].username); + + if (slot_index + 1 < shard.count) { + memmove(&shard.users[slot_index], &shard.users[slot_index + 1], + (shard.count - slot_index - 1) * sizeof(shard.users[0])); + } + shard.count--; + + return save_user_shard(state, shard_index, &shard); +} + +hybbx_result_t hybbx_storage_flatfile_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + struct flatfile_state *state = storage->backend_data; + char line[384]; + time_t now = time(NULL); + hybbx_result_t rc; + + if (state == NULL || user == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + rc = bump_counter(state->session_next_path, &out->session_id); + if (rc != HYBBX_OK) { + return rc; + } + + out->user_id = user->id; + hybbx_strlcpy(out->username, user->username, sizeof(out->username)); + hybbx_strlcpy(out->transport, transport, sizeof(out->transport)); + out->connected_at = now; + out->active = 1; + + snprintf(line, sizeof(line), "%llu|%llu|%s|%s|%lld|0|1", + (unsigned long long)out->session_id, + (unsigned long long)out->user_id, + out->username, out->transport, (long long)out->connected_at); + + return append_line(state->sessions_path, line); +} + +hybbx_result_t hybbx_storage_flatfile_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + struct flatfile_state *state = storage->backend_data; + char line[384]; + time_t now = time(NULL); + + if (state == NULL || session_id == 0) { + return HYBBX_ERR_INVALID; + } + + snprintf(line, sizeof(line), "%llu|0|_|_|0|%lld|0", + (unsigned long long)session_id, (long long)now); + + return append_line(state->sessions_path, line); +} diff --git a/src/core/storage_private.h b/src/core/storage_private.h new file mode 100644 index 0000000..579250c --- /dev/null +++ b/src/core/storage_private.h @@ -0,0 +1,81 @@ +#ifndef HYBBX_STORAGE_PRIVATE_H +#define HYBBX_STORAGE_PRIVATE_H + +#include "hybbx/auth.h" +#include "hybbx/storage.h" + +#include + +struct hybbx_storage { + hybbx_storage_backend_kind_t backend; + char *path; + char guest_prefix[HYBBX_AUTH_GUEST_PREFIX_MAX]; + hybbx_storage_sql_config_t sql_cfg; + void *backend_data; +}; + +hybbx_result_t hybbx_storage_flatfile_open(hybbx_storage_t *storage); +void hybbx_storage_flatfile_close(hybbx_storage_t *storage); +hybbx_result_t hybbx_storage_flatfile_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out); +hybbx_result_t hybbx_storage_flatfile_session_end(hybbx_storage_t *storage, + uint64_t session_id); +hybbx_result_t hybbx_storage_flatfile_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_flatfile_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_flatfile_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count); +hybbx_result_t hybbx_storage_flatfile_foreach_user( + hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx); +hybbx_result_t hybbx_storage_flatfile_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_flatfile_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user); +hybbx_result_t hybbx_storage_flatfile_delete_user(hybbx_storage_t *storage, + uint64_t user_id); + +hybbx_result_t hybbx_storage_sql_open(hybbx_storage_t *storage); +void hybbx_storage_sql_close(hybbx_storage_t *storage); +hybbx_result_t hybbx_storage_sql_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_sql_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_sql_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out); +hybbx_result_t hybbx_storage_sql_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count); +hybbx_result_t hybbx_storage_sql_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx); +hybbx_result_t hybbx_storage_sql_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user); +hybbx_result_t hybbx_storage_sql_delete_user(hybbx_storage_t *storage, + uint64_t user_id); +hybbx_result_t hybbx_storage_sql_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out); +hybbx_result_t hybbx_storage_sql_session_end(hybbx_storage_t *storage, + uint64_t session_id); +void hybbx_storage_sql_backup_files(const hybbx_storage_t *storage); +void hybbx_storage_sql_backup_tick(hybbx_storage_t *storage); + +#ifdef HYBBX_HAVE_SQLITE +struct sqlite3; +struct sqlite3 *hybbx_storage_sql_mail_db(hybbx_storage_t *storage); +#endif + +#endif /* HYBBX_STORAGE_PRIVATE_H */ diff --git a/src/core/storage_sql.c b/src/core/storage_sql.c new file mode 100644 index 0000000..4a5dc13 --- /dev/null +++ b/src/core/storage_sql.c @@ -0,0 +1,1209 @@ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/config.h" +#include "hybbx/password.h" +#include "hybbx/security.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "storage_private.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HYBBX_HAVE_SQLITE +#include +#endif + +#define HYBBX_SQL_DEFAULT_USER_DB "users.db" +#define HYBBX_SQL_DEFAULT_MAIL_DB "mail.db" + +#ifndef HYBBX_HAVE_SQLITE + +hybbx_result_t hybbx_storage_sql_open(hybbx_storage_t *storage) +{ + (void)storage; + hybbx_log_warn("[storage] SQLite backend requested but hybbx was built without " + "libsqlite3 — use backend=flatfile or rebuild with sqlite3"); + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_storage_sql_close(hybbx_storage_t *storage) +{ + (void)storage; +} + +hybbx_result_t hybbx_storage_sql_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + (void)storage; + (void)reg; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + (void)storage; + (void)username; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + (void)storage; + (void)name; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + (void)storage; + (void)level; + (void)count; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + (void)storage; + (void)fn; + (void)ctx; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + (void)storage; + (void)user; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + (void)storage; + (void)user_id; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + (void)storage; + (void)user; + (void)transport; + (void)out; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t hybbx_storage_sql_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + (void)storage; + (void)session_id; + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_storage_sql_backup_files(const hybbx_storage_t *storage) +{ + (void)storage; +} + +void hybbx_storage_sql_backup_tick(hybbx_storage_t *storage) +{ + (void)storage; +} + +#else /* HYBBX_HAVE_SQLITE */ + +struct sql_state { + sqlite3 *users_db; + sqlite3 *mail_db; + unsigned backup_tick; +}; + +static int sql_str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static hybbx_result_t sql_exec(sqlite3 *db, const char *sql) +{ + char *err = NULL; + int rc; + + if (db == NULL || sql == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_exec(db, sql, NULL, NULL, &err); + if (rc != SQLITE_OK) { + hybbx_log_warn("[storage] sqlite: %s", + err != NULL ? err : sqlite3_errmsg(db)); + sqlite3_free(err); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t sql_meta_get(sqlite3 *db, const char *key, uint64_t *value) +{ + sqlite3_stmt *stmt; + int rc; + + *value = 0; + + rc = sqlite3_prepare_v2(db, + "SELECT value FROM meta WHERE key = ?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *value = (uint64_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + return HYBBX_OK; +} + +static hybbx_result_t sql_meta_set(sqlite3 *db, const char *key, uint64_t value) +{ + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_prepare_v2(db, + "INSERT INTO meta(key,value) VALUES(?1,?2) " + "ON CONFLICT(key) DO UPDATE SET value=?2;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)value); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +static hybbx_result_t sql_meta_bump(sqlite3 *db, const char *key, uint64_t *value) +{ + hybbx_result_t rc; + + rc = sql_meta_get(db, key, value); + if (rc != HYBBX_OK) { + return rc; + } + + (*value)++; + return sql_meta_set(db, key, *value); +} + +static hybbx_result_t sql_open_db(const char *path, sqlite3 **out_db) +{ + int rc; + int existed = (access(path, F_OK) == 0); + + if (out_db == NULL) { + return HYBBX_ERR_INVALID; + } + + *out_db = NULL; + rc = sqlite3_open(path, out_db); + if (rc != SQLITE_OK) { + hybbx_log_warn("[storage] cannot open '%s': %s", + path, sqlite3_errmsg(*out_db)); + if (*out_db != NULL) { + sqlite3_close(*out_db); + *out_db = NULL; + } + return HYBBX_ERR_IO; + } + + sqlite3_busy_timeout(*out_db, 5000); + if (!existed) { + hybbx_log_info("[storage] created new database %s", path); + } else { + hybbx_log_info("[storage] opened existing database %s", path); + } + + return HYBBX_OK; +} + +static hybbx_result_t sql_init_users_schema(sqlite3 *db) +{ + static const char *schema = + "CREATE TABLE IF NOT EXISTS users (" + " id INTEGER PRIMARY KEY," + " username TEXT NOT NULL UNIQUE," + " nickname TEXT," + " level INTEGER NOT NULL," + " active INTEGER NOT NULL," + " created_at INTEGER NOT NULL," + " full_name TEXT," + " country TEXT," + " location TEXT," + " email TEXT," + " password TEXT NOT NULL," + " last_login_at INTEGER" + ");" + "CREATE INDEX IF NOT EXISTS idx_users_nickname ON users(nickname);" + "CREATE TABLE IF NOT EXISTS sessions (" + " session_id INTEGER PRIMARY KEY," + " user_id INTEGER NOT NULL," + " username TEXT," + " transport TEXT," + " remote TEXT," + " connected_at INTEGER," + " disconnected_at INTEGER," + " active INTEGER" + ");" + "CREATE TABLE IF NOT EXISTS meta (" + " key TEXT PRIMARY KEY," + " value INTEGER NOT NULL" + ");"; + + return sql_exec(db, schema); +} + +static hybbx_result_t sql_init_mail_schema(sqlite3 *db) +{ + static const char *schema = + "CREATE TABLE IF NOT EXISTS messages (" + " id INTEGER PRIMARY KEY," + " owner TEXT NOT NULL," + " from_user TEXT NOT NULL," + " subject TEXT," + " body TEXT," + " received_at INTEGER NOT NULL," + " read_flag INTEGER NOT NULL DEFAULT 0," + " deleted_at INTEGER," + " folder INTEGER NOT NULL DEFAULT 0" + ");" + "CREATE INDEX IF NOT EXISTS idx_mail_owner ON messages(owner, folder, received_at DESC);" + "CREATE TABLE IF NOT EXISTS meta (" + " key TEXT PRIMARY KEY," + " value INTEGER NOT NULL" + ");"; + + return sql_exec(db, schema); +} + +static void sql_row_to_user(sqlite3_stmt *stmt, hybbx_user_record_t *user) +{ + memset(user, 0, sizeof(*user)); + user->id = (uint64_t)sqlite3_column_int64(stmt, 0); + hybbx_strlcpy(user->username, (const char *)sqlite3_column_text(stmt, 1), + sizeof(user->username)); + if (sqlite3_column_text(stmt, 2) != NULL) { + hybbx_strlcpy(user->nickname, (const char *)sqlite3_column_text(stmt, 2), + sizeof(user->nickname)); + } + user->level = (hybbx_user_level_t)sqlite3_column_int(stmt, 3); + user->active = sqlite3_column_int(stmt, 4); + user->created_at = (time_t)sqlite3_column_int64(stmt, 5); + if (sqlite3_column_text(stmt, 6) != NULL) { + hybbx_strlcpy(user->full_name, (const char *)sqlite3_column_text(stmt, 6), + sizeof(user->full_name)); + } + if (sqlite3_column_text(stmt, 7) != NULL) { + hybbx_strlcpy(user->country, (const char *)sqlite3_column_text(stmt, 7), + sizeof(user->country)); + } + if (sqlite3_column_text(stmt, 8) != NULL) { + hybbx_strlcpy(user->location, (const char *)sqlite3_column_text(stmt, 8), + sizeof(user->location)); + } + if (sqlite3_column_text(stmt, 9) != NULL) { + hybbx_strlcpy(user->email, (const char *)sqlite3_column_text(stmt, 9), + sizeof(user->email)); + } + if (sqlite3_column_text(stmt, 10) != NULL) { + hybbx_strlcpy(user->password, (const char *)sqlite3_column_text(stmt, 10), + sizeof(user->password)); + } + user->last_login_at = (time_t)sqlite3_column_int64(stmt, 11); +} + +static hybbx_result_t sql_ensure_default_sysop(hybbx_storage_t *storage) +{ + struct sql_state *state = storage->backend_data; + size_t sysop_count = 0; + hybbx_user_record_t sysop; + char plain_password[HYBBX_PASSWORD_MAX_LEN + 1]; + uint64_t user_id; + sqlite3_stmt *stmt; + int rc; + hybbx_result_t hres; + + hres = hybbx_storage_sql_count_level(storage, HYBBX_LEVEL_SYSOP, &sysop_count); + if (hres != HYBBX_OK) { + return hres; + } + if (sysop_count > 0) { + return HYBBX_OK; + } + + hres = sql_meta_bump(state->users_db, "user_next", &user_id); + if (hres != HYBBX_OK) { + return hres; + } + + memset(&sysop, 0, sizeof(sysop)); + sysop.id = user_id; + hybbx_strlcpy(sysop.username, "sysop", sizeof(sysop.username)); + hybbx_strlcpy(sysop.nickname, HYBBX_DEFAULT_SYSOP_USERNAME, + sizeof(sysop.nickname)); + sysop.level = HYBBX_LEVEL_SYSOP; + sysop.active = 1; + sysop.created_at = time(NULL); + hybbx_strlcpy(sysop.full_name, "System Operator", sizeof(sysop.full_name)); + + if (hybbx_password_generate_alnum(plain_password, sizeof(plain_password), + HYBBX_SYSOP_INIT_PASSWORD_MIN, + HYBBX_SYSOP_INIT_PASSWORD_MAX) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (hybbx_password_hash(plain_password, sysop.password, + sizeof(sysop.password)) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO users(id,username,nickname,level,active," + "created_at,full_name,country,location,email,password," + "last_login_at) VALUES(?1,?2,?3,?4,?5,?6,?7,'','','',?8,0);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)sysop.id); + sqlite3_bind_text(stmt, 2, sysop.username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, sysop.nickname, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 4, (int)sysop.level); + sqlite3_bind_int(stmt, 5, sysop.active); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)sysop.created_at); + sqlite3_bind_text(stmt, 7, sysop.full_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 8, sysop.password, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + + hybbx_log_info("[storage] created default Sysop in %s (login: %s / %s)", + storage->sql_cfg.user_db, sysop.nickname, plain_password); + hybbx_security_log_write("sysop_created user=%s", sysop.nickname); + return HYBBX_OK; +} + +static hybbx_result_t sql_copy_file(const char *src, const char *dst) +{ + FILE *in_fp; + FILE *out_fp; + unsigned char buf[4096]; + size_t n; + + in_fp = fopen(src, "rb"); + if (in_fp == NULL) { + return HYBBX_ERR_IO; + } + + out_fp = fopen(dst, "wb"); + if (out_fp == NULL) { + fclose(in_fp); + return HYBBX_ERR_IO; + } + + while ((n = fread(buf, 1, sizeof(buf), in_fp)) > 0) { + if (fwrite(buf, 1, n, out_fp) != n) { + fclose(in_fp); + fclose(out_fp); + return HYBBX_ERR_IO; + } + } + + fclose(in_fp); + if (fclose(out_fp) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void sql_backup_one(sqlite3 *db, const char *src_path, + const char *backup_dir) +{ + char dst[HYBBX_PATH_MAX]; + const char *base; + hybbx_result_t rc; + + if (db == NULL || src_path == NULL || src_path[0] == '\0') { + return; + } + + (void)sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL); + + base = strrchr(src_path, '/'); + base = (base != NULL) ? base + 1 : src_path; + + if (backup_dir != NULL && backup_dir[0] != '\0') { + char name[HYBBX_PATH_MAX]; + + if (mkdir_p(backup_dir) != 0) { + return; + } + if (strlen(base) + strlen(HYBBX_STORAGE_BACKUP_SUFFIX) + 1 >= + sizeof(name)) { + return; + } + snprintf(name, sizeof(name), "%s%s", base, HYBBX_STORAGE_BACKUP_SUFFIX); + if (hybbx_path_join(dst, sizeof(dst), backup_dir, name) != HYBBX_OK) { + return; + } + } else { + if (strlen(src_path) + strlen(HYBBX_STORAGE_BACKUP_SUFFIX) + 1 >= + sizeof(dst)) { + return; + } + snprintf(dst, sizeof(dst), "%s%s", src_path, HYBBX_STORAGE_BACKUP_SUFFIX); + } + + rc = sql_copy_file(src_path, dst); + if (rc != HYBBX_OK) { + hybbx_log_warn("[storage] backup failed %s -> %s", src_path, dst); + } +} + +hybbx_result_t hybbx_storage_sql_open(hybbx_storage_t *storage) +{ + struct sql_state *state; + hybbx_result_t rc; + + if (storage == NULL || storage->path == NULL) { + return HYBBX_ERR_INVALID; + } + + if (storage->backend == HYBBX_STORAGE_MYSQL || + storage->backend == HYBBX_STORAGE_MARIADB) { + hybbx_log_warn("[storage] MySQL/MariaDB backends are not supported — " + "use flatfile or sqlite"); + return HYBBX_ERR_UNSUPPORTED; + } + + if (mkdir_p(storage->path) != 0) { + hybbx_log_warn("[storage] cannot create data path '%s'", + storage->path); + return HYBBX_ERR_IO; + } + + state = calloc(1, sizeof(*state)); + if (state == NULL) { + return HYBBX_ERR_NOMEM; + } + + rc = sql_open_db(storage->sql_cfg.user_db, &state->users_db); + if (rc != HYBBX_OK) { + free(state); + return rc; + } + + rc = sql_init_users_schema(state->users_db); + if (rc != HYBBX_OK) { + hybbx_storage_sql_close(storage); + free(state); + return rc; + } + + rc = sql_open_db(storage->sql_cfg.mail_db, &state->mail_db); + if (rc != HYBBX_OK) { + sqlite3_close(state->users_db); + free(state); + return rc; + } + + rc = sql_init_mail_schema(state->mail_db); + if (rc != HYBBX_OK) { + sqlite3_close(state->users_db); + sqlite3_close(state->mail_db); + free(state); + return rc; + } + + storage->backend_data = state; + + rc = sql_ensure_default_sysop(storage); + if (rc != HYBBX_OK) { + hybbx_storage_sql_close(storage); + return rc; + } + + hybbx_log_info("[storage] sqlite user_db=%s mail_db=%s backup_interval=%us", + storage->sql_cfg.user_db, storage->sql_cfg.mail_db, + storage->sql_cfg.backup_interval_sec); + + return HYBBX_OK; +} + +void hybbx_storage_sql_close(hybbx_storage_t *storage) +{ + struct sql_state *state; + + if (storage == NULL) { + return; + } + + state = storage->backend_data; + if (state == NULL) { + return; + } + + if (state->users_db != NULL) { + sqlite3_close(state->users_db); + } + if (state->mail_db != NULL) { + sqlite3_close(state->mail_db); + } + + free(state); + storage->backend_data = NULL; +} + +sqlite3 *hybbx_storage_sql_mail_db(hybbx_storage_t *storage) +{ + struct sql_state *state; + + if (storage == NULL) { + return NULL; + } + + state = storage->backend_data; + if (state == NULL) { + return NULL; + } + + return state->mail_db; +} + +static hybbx_result_t sql_find_user_by_column(sqlite3 *db, const char *column, + const char *name, + hybbx_user_record_t *out) +{ + sqlite3_stmt *stmt; + int rc; + const char *sql_username = + "SELECT id,username,nickname,level,active,created_at,full_name," + "country,location,email,password,last_login_at " + "FROM users WHERE lower(username)=lower(?1) LIMIT 1;"; + const char *sql_nickname = + "SELECT id,username,nickname,level,active,created_at,full_name," + "country,location,email,password,last_login_at " + "FROM users WHERE lower(nickname)=lower(?1) LIMIT 1;"; + const char *sql = sql_str_ieq(column, "nickname") ? sql_nickname : + sql_username; + + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + sql_row_to_user(stmt, out); + sqlite3_finalize(stmt); + return HYBBX_OK; + } + + sqlite3_finalize(stmt); + return HYBBX_ERR_NOT_FOUND; +} + +hybbx_result_t hybbx_storage_sql_find_user(hybbx_storage_t *storage, + const char *username, + hybbx_user_record_t *out) +{ + struct sql_state *state; + char normalized[HYBBX_USER_NAME_MAX]; + + if (storage == NULL || username == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_strlcpy(normalized, username, sizeof(normalized)); + hybbx_username_normalize(normalized); + return sql_find_user_by_column(state->users_db, "username", normalized, out); +} + +hybbx_result_t hybbx_storage_sql_resolve_user(hybbx_storage_t *storage, + const char *name, + hybbx_user_record_t *out) +{ + hybbx_result_t rc; + + if (storage == NULL || name == NULL || out == NULL || name[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_storage_sql_find_user(storage, name, out); + if (rc == HYBBX_OK) { + return HYBBX_OK; + } + if (rc != HYBBX_ERR_NOT_FOUND) { + return rc; + } + + return sql_find_user_by_column( + ((struct sql_state *)storage->backend_data)->users_db, + "nickname", name, out); +} + +typedef struct foreach_ctx { + hybbx_storage_user_fn fn; + void *user_ctx; +} foreach_ctx_t; + +static int sql_foreach_cb(void *ctx, int n_cols, char **values, char **cols) +{ + hybbx_user_record_t user; + foreach_ctx_t *fctx = (foreach_ctx_t *)ctx; + hybbx_result_t rc; + int i; + + (void)n_cols; + (void)cols; + memset(&user, 0, sizeof(user)); + + for (i = 0; values[i] != NULL; i++) { + switch (i) { + case 0: user.id = (uint64_t)strtoull(values[i], NULL, 10); break; + case 1: hybbx_strlcpy(user.username, values[i], sizeof(user.username)); break; + case 2: hybbx_strlcpy(user.nickname, values[i], sizeof(user.nickname)); break; + case 3: user.level = (hybbx_user_level_t)atoi(values[i]); break; + case 4: user.active = atoi(values[i]); break; + case 5: user.created_at = (time_t)strtoll(values[i], NULL, 10); break; + case 6: hybbx_strlcpy(user.full_name, values[i], sizeof(user.full_name)); break; + case 7: hybbx_strlcpy(user.country, values[i], sizeof(user.country)); break; + case 8: hybbx_strlcpy(user.location, values[i], sizeof(user.location)); break; + case 9: hybbx_strlcpy(user.email, values[i], sizeof(user.email)); break; + case 10: hybbx_strlcpy(user.password, values[i], sizeof(user.password)); break; + case 11: user.last_login_at = (time_t)strtoll(values[i], NULL, 10); break; + default: break; + } + } + + rc = fctx->fn(&user, fctx->user_ctx); + return (rc == HYBBX_OK) ? 0 : 1; +} + +hybbx_result_t hybbx_storage_sql_foreach_user(hybbx_storage_t *storage, + hybbx_storage_user_fn fn, + void *ctx) +{ + struct sql_state *state; + foreach_ctx_t fctx; + char *err = NULL; + int rc; + + if (storage == NULL || fn == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + fctx.fn = fn; + fctx.user_ctx = ctx; + + rc = sqlite3_exec(state->users_db, + "SELECT id,username,nickname,level,active,created_at," + "full_name,country,location,email,password,last_login_at " + "FROM users ORDER BY id;", + sql_foreach_cb, &fctx, &err); + if (rc == SQLITE_ABORT) { + sqlite3_free(err); + return HYBBX_ERR_BUSY; + } + if (rc != SQLITE_OK) { + hybbx_log_warn("[storage] foreach: %s", err != NULL ? err : "error"); + sqlite3_free(err); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_count_level(hybbx_storage_t *storage, + hybbx_user_level_t level, + size_t *count) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + + if (storage == NULL || count == NULL) { + return HYBBX_ERR_INVALID; + } + + *count = 0; + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "SELECT COUNT(*) FROM users WHERE level=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int(stmt, 1, (int)level); + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + *count = (size_t)sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + return HYBBX_OK; +} + +typedef struct identity_taken_ctx { + const char *username; + const char *nickname; + int taken; +} identity_taken_ctx_t; + +static hybbx_result_t identity_taken_cb(const hybbx_user_record_t *user, void *ctx) +{ + identity_taken_ctx_t *ictx = (identity_taken_ctx_t *)ctx; + + if (sql_str_ieq(user->username, ictx->username) || + sql_str_ieq(user->username, ictx->nickname) || + (user->nickname[0] != '\0' && + (sql_str_ieq(user->nickname, ictx->nickname) || + sql_str_ieq(user->nickname, ictx->username)))) { + ictx->taken = 1; + return HYBBX_ERR_BUSY; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_register_user(hybbx_storage_t *storage, + const hybbx_user_registration_t *reg, + hybbx_user_record_t *out) +{ + struct sql_state *state; + hybbx_user_record_t existing; + identity_taken_ctx_t taken; + uint64_t user_id; + sqlite3_stmt *stmt; + int rc; + hybbx_result_t hres; + + if (storage == NULL || reg == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!hybbx_registration_valid(reg, storage->guest_prefix)) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + hres = hybbx_storage_sql_find_user(storage, reg->username, &existing); + if (hres == HYBBX_OK) { + return HYBBX_ERR_BUSY; + } + if (hres != HYBBX_ERR_NOT_FOUND) { + return hres; + } + + memset(&taken, 0, sizeof(taken)); + taken.username = reg->username; + taken.nickname = reg->nickname; + hres = hybbx_storage_sql_foreach_user(storage, identity_taken_cb, &taken); + if (hres == HYBBX_ERR_BUSY || taken.taken) { + return HYBBX_ERR_BUSY; + } + if (hres != HYBBX_OK) { + return hres; + } + + hres = sql_meta_bump(state->users_db, "user_next", &user_id); + if (hres != HYBBX_OK) { + return hres; + } + + memset(out, 0, sizeof(*out)); + out->id = user_id; + hybbx_strlcpy(out->username, reg->username, sizeof(out->username)); + hybbx_username_normalize(out->username); + hybbx_strlcpy(out->nickname, reg->nickname, sizeof(out->nickname)); + out->level = HYBBX_LEVEL_USER; + out->active = 0; + out->created_at = time(NULL); + hybbx_strlcpy(out->full_name, reg->full_name, sizeof(out->full_name)); + hybbx_strlcpy(out->country, reg->country, sizeof(out->country)); + hybbx_strlcpy(out->location, reg->location, sizeof(out->location)); + hybbx_strlcpy(out->email, reg->email, sizeof(out->email)); + + if (reg->password[0] != '\0') { + if (!hybbx_password_plain_valid(reg->password)) { + return HYBBX_ERR_INVALID; + } + hres = hybbx_password_hash(reg->password, out->password, + sizeof(out->password)); + if (hres != HYBBX_OK) { + return hres; + } + } + + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO users(id,username,nickname,level,active," + "created_at,full_name,country,location,email,password," + "last_login_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,0);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)out->id); + sqlite3_bind_text(stmt, 2, out->username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, out->nickname, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 4, (int)out->level); + sqlite3_bind_int(stmt, 5, out->active); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)out->created_at); + sqlite3_bind_text(stmt, 7, out->full_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 8, out->country, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 9, out->location, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 10, out->email, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 11, out->password, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_storage_sql_update_user(hybbx_storage_t *storage, + const hybbx_user_record_t *user) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + + if (storage == NULL || user == NULL || user->id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "UPDATE users SET username=?2,nickname=?3,level=?4," + "active=?5,created_at=?6,full_name=?7,country=?8," + "location=?9,email=?10,password=?11,last_login_at=?12 " + "WHERE id=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)user->id); + sqlite3_bind_text(stmt, 2, user->username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, user->nickname, -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 4, (int)user->level); + sqlite3_bind_int(stmt, 5, user->active); + sqlite3_bind_int64(stmt, 6, (sqlite3_int64)user->created_at); + sqlite3_bind_text(stmt, 7, user->full_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 8, user->country, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 9, user->location, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 10, user->email, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 11, user->password, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 12, (sqlite3_int64)user->last_login_at); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + if (sqlite3_changes(state->users_db) == 0) { + return HYBBX_ERR_NOT_FOUND; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_delete_user(hybbx_storage_t *storage, + uint64_t user_id) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + + if (storage == NULL || user_id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "DELETE FROM users WHERE id=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)user_id); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + if (sqlite3_changes(state->users_db) == 0) { + return HYBBX_ERR_NOT_FOUND; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_storage_sql_session_begin(hybbx_storage_t *storage, + const hybbx_user_record_t *user, + const char *transport, + hybbx_session_record_t *out) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + hybbx_result_t hres; + + if (storage == NULL || user == NULL || transport == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + hres = sql_meta_bump(state->users_db, "session_next", &out->session_id); + if (hres != HYBBX_OK) { + return hres; + } + + out->user_id = user->id; + hybbx_strlcpy(out->username, user->username, sizeof(out->username)); + hybbx_strlcpy(out->transport, transport, sizeof(out->transport)); + out->connected_at = time(NULL); + out->active = 1; + + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO sessions(session_id,user_id,username," + "transport,remote,connected_at,disconnected_at,active) " + "VALUES(?1,?2,?3,?4,'',?5,0,1);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)out->session_id); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)out->user_id); + sqlite3_bind_text(stmt, 3, out->username, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, out->transport, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 5, (sqlite3_int64)out->connected_at); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? HYBBX_OK : HYBBX_ERR_IO; +} + +hybbx_result_t hybbx_storage_sql_session_end(hybbx_storage_t *storage, + uint64_t session_id) +{ + struct sql_state *state; + sqlite3_stmt *stmt; + int rc; + time_t now = time(NULL); + + if (storage == NULL || session_id == 0) { + return HYBBX_ERR_INVALID; + } + + state = storage->backend_data; + if (state == NULL || state->users_db == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = sqlite3_prepare_v2(state->users_db, + "UPDATE sessions SET disconnected_at=?2, active=0 " + "WHERE session_id=?1;", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)session_id); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)now); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + + if (sqlite3_changes(state->users_db) == 0) { + rc = sqlite3_prepare_v2(state->users_db, + "INSERT INTO sessions(session_id,user_id,username," + "transport,remote,connected_at,disconnected_at," + "active) VALUES(?1,0,'','','',0,?2,0);", + -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return HYBBX_ERR_IO; + } + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)session_id); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)now); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} + +void hybbx_storage_sql_backup_files(const hybbx_storage_t *storage) +{ + struct sql_state *state; + const char *backup_dir; + + if (storage == NULL || storage->backend != HYBBX_STORAGE_SQLITE) { + return; + } + + state = storage->backend_data; + if (state == NULL) { + return; + } + + backup_dir = storage->sql_cfg.backup_path; + if (backup_dir != NULL && backup_dir[0] == '\0') { + backup_dir = NULL; + } + + sql_backup_one(state->users_db, storage->sql_cfg.user_db, backup_dir); + sql_backup_one(state->mail_db, storage->sql_cfg.mail_db, backup_dir); +} + +void hybbx_storage_sql_backup_tick(hybbx_storage_t *storage) +{ + struct sql_state *state; + + if (storage == NULL || storage->backend != HYBBX_STORAGE_SQLITE) { + return; + } + + state = storage->backend_data; + if (state == NULL || storage->sql_cfg.backup_interval_sec == 0) { + return; + } + + state->backup_tick++; + if (state->backup_tick >= storage->sql_cfg.backup_interval_sec) { + state->backup_tick = 0; + hybbx_storage_sql_backup_files(storage); + } +} + +#endif /* HYBBX_HAVE_SQLITE */ diff --git a/src/core/terminal.c b/src/core/terminal.c new file mode 100644 index 0000000..b4929f4 --- /dev/null +++ b/src/core/terminal.c @@ -0,0 +1,98 @@ +#include "hybbx/terminal.h" +#include "hybbx/session.h" +#include "hybbx/traffic.h" +#include "hybbx/limits.h" + +#include + +static int term_is_csi_terminator(unsigned char ch) +{ + return ch >= 0x40 && ch <= 0x7E; +} + +size_t hybbx_term_copy_plain(const char *src, char *dst, size_t dst_size) +{ + size_t di = 0; + size_t si = 0; + + if (dst == NULL || dst_size == 0) { + return 0; + } + + dst[0] = '\0'; + + if (src == NULL) { + return 0; + } + + while (src[si] != '\0' && di + 1 < dst_size) { + unsigned char ch = (unsigned char)src[si]; + + if (ch == 0x1Bu) { + si++; + if (src[si] == '\0') { + break; + } + + if (src[si] == '[') { + si++; + while (src[si] != '\0' && !term_is_csi_terminator((unsigned char)src[si])) { + si++; + } + if (src[si] != '\0') { + si++; + } + } else { + si++; + } + continue; + } + + dst[di++] = (char)ch; + si++; + } + + dst[di] = '\0'; + return di; +} + +hybbx_result_t hybbx_term_init_session(hybbx_session_t *session) +{ + const hybbx_traffic_config_t *traffic; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + traffic = hybbx_traffic_config_get(); + if (traffic == NULL || !traffic->ansi) { + return HYBBX_OK; + } + + return hybbx_session_write(session, HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK); +} + +hybbx_result_t hybbx_term_clear_screen(hybbx_session_t *session) +{ + const hybbx_traffic_config_t *traffic; + int i; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + traffic = hybbx_traffic_config_get(); + if (traffic != NULL && traffic->ansi) { + return hybbx_session_write(session, HYBBX_TERM_CLEAR_SCREEN); + } + + for (i = 0; i < 24; i++) { + hybbx_result_t rc = hybbx_session_write(session, "\n"); + + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} diff --git a/src/core/texts.c b/src/core/texts.c new file mode 100644 index 0000000..f983744 --- /dev/null +++ b/src/core/texts.c @@ -0,0 +1,470 @@ +#include "hybbx/texts.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/hybbx.h" + +#include +#include +#include + +void hybbx_texts_config_defaults(hybbx_texts_config_t *texts) +{ + if (texts == NULL) { + return; + } + + hybbx_strlcpy(texts->path, HYBBX_DEFAULT_TEXTS_PATH, sizeof(texts->path)); +} + +hybbx_result_t hybbx_texts_resolve(const hybbx_texts_config_t *texts, + const char *filename, + char *out, size_t out_len) +{ + if (texts == NULL || filename == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_path_join(out, out_len, texts->path, filename); +} + +static size_t append_token_expanded(char *out, size_t out_len, size_t pos, + const char *value) +{ + size_t value_len; + + if (out == NULL || out_len == 0 || value == NULL) { + return pos; + } + + value_len = strlen(value); + + if (pos + value_len >= out_len) { + return out_len - 1; + } + + memcpy(out + pos, value, value_len); + return pos + value_len; +} + +static void expand_text_line(char *out, size_t out_len, + const char *line, + const char *version, + const char *service_name, + const char *username, + const char *os_name, + const char *time_text, + const char *date_text) +{ + size_t pos = 0; + size_t i = 0; + + if (out == NULL || out_len == 0 || line == NULL) { + return; + } + + out[0] = '\0'; + + if (version == NULL) { + version = HYBBX_VERSION_STRING; + } + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + if (username == NULL) { + username = ""; + } + if (os_name == NULL) { + os_name = ""; + } + if (time_text == NULL) { + time_text = ""; + } + if (date_text == NULL) { + date_text = ""; + } + + while (line[i] != '\0' && pos + 1 < out_len) { + if (strncmp(line + i, HYBBX_BANNER_TOKEN_VERSION, + strlen(HYBBX_BANNER_TOKEN_VERSION)) == 0) { + pos = append_token_expanded(out, out_len, pos, version); + i += strlen(HYBBX_BANNER_TOKEN_VERSION); + continue; + } + + if (strncmp(line + i, HYBBX_BANNER_TOKEN_SERVICE, + strlen(HYBBX_BANNER_TOKEN_SERVICE)) == 0) { + pos = append_token_expanded(out, out_len, pos, service_name); + i += strlen(HYBBX_BANNER_TOKEN_SERVICE); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_USERNAME, + strlen(HYBBX_TEXT_TOKEN_USERNAME)) == 0) { + pos = append_token_expanded(out, out_len, pos, username); + i += strlen(HYBBX_TEXT_TOKEN_USERNAME); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_OS, + strlen(HYBBX_TEXT_TOKEN_OS)) == 0) { + pos = append_token_expanded(out, out_len, pos, os_name); + i += strlen(HYBBX_TEXT_TOKEN_OS); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_TIME, + strlen(HYBBX_TEXT_TOKEN_TIME)) == 0) { + pos = append_token_expanded(out, out_len, pos, time_text); + i += strlen(HYBBX_TEXT_TOKEN_TIME); + continue; + } + + if (strncmp(line + i, HYBBX_TEXT_TOKEN_DATE, + strlen(HYBBX_TEXT_TOKEN_DATE)) == 0) { + pos = append_token_expanded(out, out_len, pos, date_text); + i += strlen(HYBBX_TEXT_TOKEN_DATE); + continue; + } + + out[pos++] = line[i++]; + } + + out[pos] = '\0'; +} + +static void format_text_tokens(char *time_text, size_t time_len, + char *date_text, size_t date_len, + const struct tm *tm) +{ + if (time_text != NULL && time_len > 0) { + time_text[0] = '\0'; + if (tm != NULL) { + (void)hybbx_time_format_time(time_text, time_len, tm, NULL); + } + } + + if (date_text != NULL && date_len > 0) { + date_text[0] = '\0'; + if (tm != NULL) { + (void)hybbx_time_format_date(date_text, date_len, tm, NULL); + } + } +} + +static void fill_os_name(char *os_name, size_t os_len) +{ + if (os_name == NULL || os_len == 0) { + return; + } + + if (hybbx_platform_os_name(os_name, os_len) != HYBBX_OK) { + hybbx_strlcpy(os_name, "unknown", os_len); + } +} + +static void expand_banner_line(char *out, size_t out_len, + const char *line, + const char *version, + const char *service_name, + const char *time_text, + const char *date_text) +{ + expand_text_line(out, out_len, line, version, service_name, NULL, NULL, + time_text, date_text); +} + +typedef struct texts_line_emitter { + hybbx_session_t *session; + unsigned pending_blank; +} texts_line_emitter_t; + +static void texts_emit_expanded_line(texts_line_emitter_t *emitter, + const char *expanded) +{ + unsigned i; + + if (emitter == NULL || emitter->session == NULL || expanded == NULL) { + return; + } + + if (expanded[0] == '\0') { + emitter->pending_blank++; + return; + } + + for (i = 0; i < emitter->pending_blank; i++) { + hybbx_session_write(emitter->session, "\n"); + } + emitter->pending_blank = 0; + hybbx_session_write_line(emitter->session, expanded); +} + +static void texts_trim_trailing_cr(char *expanded) +{ + size_t n; + + if (expanded == NULL) { + return; + } + + n = strlen(expanded); + while (n > 0 && (expanded[n - 1] == '\n' || expanded[n - 1] == '\r')) { + expanded[--n] = '\0'; + } +} + +static hybbx_result_t send_banner_fallback(hybbx_session_t *session, + const char *version, + const char *service_name) +{ + char line[HYBBX_LINE_MAX]; + + if (version == NULL) { + version = HYBBX_VERSION_STRING; + } + if (service_name == NULL || service_name[0] == '\0') { + service_name = HYBBX_DEFAULT_SERVICE_NAME; + } + + snprintf(line, sizeof(line), "HyBBX %s", version); + hybbx_session_write_line(session, line); + snprintf(line, sizeof(line), "Online at %s", service_name); + hybbx_session_write_line(session, line); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_banner(const hybbx_texts_config_t *texts, + hybbx_session_t *session, + const char *version, + const char *service_name) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + hybbx_result_t rc; + + if (texts == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + + rc = hybbx_texts_resolve(texts, HYBBX_TEXT_BANNER, path, sizeof(path)); + if (rc != HYBBX_OK) { + return send_banner_fallback(session, version, service_name); + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return send_banner_fallback(session, version, service_name); + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_banner_line(expanded, sizeof(expanded), line, version, + service_name, time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_motd(const hybbx_texts_config_t *texts, + hybbx_session_t *session) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + const char *username; + hybbx_result_t rc; + + if (texts == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + username = hybbx_session_display_name(session); + if (username[0] == '\0') { + username = "visitor"; + } + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + + rc = hybbx_texts_resolve(texts, HYBBX_TEXT_MOTD, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_text_line(expanded, sizeof(expanded), line, NULL, NULL, + username, NULL, time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_file(const hybbx_texts_config_t *texts, + hybbx_session_t *session, + const char *filename) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + char os_name[64]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + hybbx_result_t rc; + + if (texts == NULL || session == NULL || filename == NULL) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + fill_os_name(os_name, sizeof(os_name)); + + rc = hybbx_texts_resolve(texts, filename, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return HYBBX_ERR_NOT_FOUND; + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_text_line(expanded, sizeof(expanded), line, + HYBBX_VERSION_STRING, NULL, NULL, os_name, + time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t send_version_fallback(hybbx_session_t *session, + const char *os_name) +{ + char line[HYBBX_LINE_MAX]; + + snprintf(line, sizeof(line), "HyBBX %s", HYBBX_VERSION_STRING); + hybbx_session_write_line(session, line); + snprintf(line, sizeof(line), "Operating system: %s", + (os_name != NULL && os_name[0] != '\0') ? os_name : "unknown"); + hybbx_session_write_line(session, line); + return HYBBX_OK; +} + +hybbx_result_t hybbx_texts_send_version(const hybbx_texts_config_t *texts, + hybbx_session_t *session) +{ + char path[HYBBX_PATH_MAX]; + char expanded[HYBBX_LINE_MAX]; + FILE *fp; + char line[HYBBX_LINE_MAX]; + char time_text[32]; + char date_text[32]; + char os_name[64]; + struct tm now_tm; + const struct tm *tm_ptr = NULL; + hybbx_result_t rc; + + if (texts == NULL || session == NULL) { + return HYBBX_ERR_INVALID; + } + + fill_os_name(os_name, sizeof(os_name)); + + if (hybbx_time_local_now(&now_tm) == HYBBX_OK) { + tm_ptr = &now_tm; + } + format_text_tokens(time_text, sizeof(time_text), + date_text, sizeof(date_text), tm_ptr); + + rc = hybbx_texts_resolve(texts, HYBBX_TEXT_VERSION, path, sizeof(path)); + if (rc != HYBBX_OK) { + return send_version_fallback(session, os_name); + } + + fp = fopen(path, "r"); + if (fp == NULL) { + return send_version_fallback(session, os_name); + } + + { + texts_line_emitter_t emitter; + + emitter.session = session; + emitter.pending_blank = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + expand_text_line(expanded, sizeof(expanded), line, + HYBBX_VERSION_STRING, NULL, NULL, os_name, + time_text, date_text); + texts_trim_trailing_cr(expanded); + texts_emit_expanded_line(&emitter, expanded); + } + } + + fclose(fp); + return HYBBX_OK; +} diff --git a/src/core/traffic.c b/src/core/traffic.c new file mode 100644 index 0000000..b74d48f --- /dev/null +++ b/src/core/traffic.c @@ -0,0 +1,294 @@ +#include "hybbx/traffic.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif +#include "hybbx/session.h" +#include "hybbx/terminal.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#include +#endif + +static hybbx_traffic_config_t g_traffic_config; +static int g_traffic_config_ready; + +void hybbx_traffic_config_defaults(hybbx_traffic_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + cfg->baud = HYBBX_BAUD2400; + cfg->line_width = HYBBX_LINE_WIDTH; + cfg->pace_output = 1; + cfg->ansi = 0; + cfg->input_echo = 0; +} + +#if !defined(HYBBX_CLIENT_BUILD) +static unsigned parse_uint_default(const hybbx_config_t *config, + const char *section, + const char *key, + unsigned default_value, + unsigned min_value, + unsigned max_value) +{ + unsigned value; + + value = hybbx_config_get_uint(config, section, key, default_value, + min_value, max_value); + return value; +} +#endif + +void hybbx_traffic_config_apply(const struct hybbx_config *config) +{ + hybbx_traffic_config_defaults(&g_traffic_config); + +#if !defined(HYBBX_CLIENT_BUILD) + if (config != NULL) { + g_traffic_config.baud = parse_uint_default( + config, "traffic", "baud", HYBBX_BAUD2400, 300u, 38400u); + g_traffic_config.line_width = parse_uint_default( + config, "traffic", "line_width", HYBBX_LINE_WIDTH, 20u, + HYBBX_LINE_WIDTH_MAX); + g_traffic_config.pace_output = + hybbx_config_get_bool(config, "traffic", "pace_output", 1); + g_traffic_config.ansi = + hybbx_config_get_bool(config, "traffic", "ansi", 0); + g_traffic_config.input_echo = + hybbx_config_get_bool(config, "traffic", "input_echo", 0); + } + + hybbx_log_info("[traffic] baud=%u line_width=%u pace=%s ansi=%s echo=%s", + g_traffic_config.baud, g_traffic_config.line_width, + hybbx_bool_to_string(g_traffic_config.pace_output), + hybbx_bool_to_string(g_traffic_config.ansi), + hybbx_bool_to_string(g_traffic_config.input_echo)); +#else + (void)config; +#endif + + g_traffic_config_ready = 1; +} + +const hybbx_traffic_config_t *hybbx_traffic_config_get(void) +{ + if (!g_traffic_config_ready) { + hybbx_traffic_config_defaults(&g_traffic_config); + g_traffic_config_ready = 1; + } + + return &g_traffic_config; +} + +unsigned hybbx_traffic_byte_delay_us(unsigned baud) +{ + if (baud == 0) { + return 0; + } + + return 10000000u / baud; +} + +static hybbx_result_t emit_raw(struct hybbx_session *session, char ch) +{ + const hybbx_traffic_config_t *cfg = hybbx_traffic_config_get(); + char byte = ch; + hybbx_result_t rc; + + if (session == NULL) { + return HYBBX_ERR_INVALID; + } + + if (session->transport != NULL && session->transport->write != NULL) { + rc = session->transport->write(session, &byte, 1); + } else { + if (fputc((unsigned char)byte, stdout) == EOF) { + return HYBBX_ERR_IO; + } + fflush(stdout); + rc = HYBBX_OK; + } + + if (rc == HYBBX_OK && cfg->pace_output && cfg->baud > 0) { + unsigned delay = hybbx_traffic_byte_delay_us(cfg->baud); + + if (delay > 0) { +#if defined(_WIN32) + Sleep((DWORD)((delay + 999u) / 1000u)); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(delay / 1000000u); +#if defined(__AMIGA__) + tv.tv_usec = (unsigned long)(delay % 1000000u); +#else + tv.tv_usec = (suseconds_t)(delay % 1000000u); +#endif + (void)select(0, NULL, NULL, NULL, &tv); +#endif + } + } + + return rc; +} + +static hybbx_result_t emit_newline(struct hybbx_session *session, + unsigned *out_col) +{ + hybbx_result_t rc; + + rc = emit_raw(session, '\r'); + if (rc != HYBBX_OK) { + return rc; + } + + rc = emit_raw(session, '\n'); + if (rc == HYBBX_OK && out_col != NULL) { + *out_col = 0; + } + + return rc; +} + +static int traffic_plain_char(const char *src, size_t len, size_t *consumed, + char *out) +{ + unsigned char ch; + + if (src == NULL || len == 0 || consumed == NULL || out == NULL) { + return 0; + } + + ch = (unsigned char)src[0]; + + if (ch == '\r') { + *consumed = 1; + *out = '\n'; + return 1; + } + + if (ch == '\n') { + *consumed = 1; + *out = '\n'; + return 1; + } + + if (ch == '\t') { + *consumed = 1; + *out = ' '; + return 1; + } + + if (ch < 0x20 || ch == 0x7f) { + *consumed = 1; + return 0; + } + + if (ch == 0x1b && len >= 2 && src[1] == '[') { + size_t i = 2; + + while (i < len && (src[i] < 0x40 || src[i] > 0x7e)) { + i++; + } + if (i < len) { + *consumed = i + 1; + } else { + *consumed = len; + } + return 0; + } + + if (ch == 0x1b) { + *consumed = 1; + return 0; + } + + *consumed = 1; + *out = (char)ch; + return 1; +} + +hybbx_result_t hybbx_traffic_emit(struct hybbx_session *session, + unsigned *out_col, + const char *data, size_t len) +{ + const hybbx_traffic_config_t *cfg = hybbx_traffic_config_get(); + size_t pos = 0; + unsigned col = out_col != NULL ? *out_col : 0; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (len == 0) { + return HYBBX_OK; + } + + if (cfg->ansi && strcmp(data, HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK) == 0 && + len == strlen(HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK)) { + size_t i; + + for (i = 0; i < len; i++) { + hybbx_result_t rc = emit_raw(session, data[i]); + + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; + } + + while (pos < len) { + char ch; + size_t step = 0; + hybbx_result_t rc; + + if (!traffic_plain_char(data + pos, len - pos, &step, &ch)) { + pos += step > 0 ? step : 1; + continue; + } + + pos += step; + + if (ch == '\n') { + rc = emit_newline(session, out_col); + if (rc != HYBBX_OK) { + return rc; + } + col = 0; + continue; + } + + if (cfg->line_width > 0 && col >= cfg->line_width) { + rc = emit_newline(session, out_col); + if (rc != HYBBX_OK) { + return rc; + } + col = 0; + } + + rc = emit_raw(session, ch); + if (rc != HYBBX_OK) { + return rc; + } + + col++; + } + + if (out_col != NULL) { + *out_col = col; + } + + return HYBBX_OK; +} diff --git a/src/core/util.c b/src/core/util.c new file mode 100644 index 0000000..1c5e9e4 --- /dev/null +++ b/src/core/util.c @@ -0,0 +1,832 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/log.h" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include +#include +#include +#include +#endif + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include +#endif + +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif + +static int bool_token_ieq(const char *value, const char *token) +{ + if (value == NULL || token == NULL) { + return 0; + } + + while (*value != '\0' && *token != '\0') { + unsigned char cv = (unsigned char)tolower((unsigned char)*value); + unsigned char ct = (unsigned char)tolower((unsigned char)*token); + + if (cv != ct) { + return 0; + } + value++; + token++; + } + + return *value == '\0' && *token == '\0'; +} + +static int bool_match_any(const char *value, const char *const *tokens, size_t count) +{ + size_t i; + + if (value == NULL || value[0] == '\0') { + return 0; + } + + for (i = 0; i < count; i++) { + if (bool_token_ieq(value, tokens[i])) { + return 1; + } + } + + return 0; +} + +static const char *const hybbx_bool_true_tokens[] = { + HYBBX_BOOL_YES, "true", "enable", "enabled", "on", "1" +}; + +static const char *const hybbx_bool_false_tokens[] = { + HYBBX_BOOL_NO, "false", "disable", "disabled", "off", "0" +}; + +int hybbx_bool_is_true(const char *value) +{ + return bool_match_any(value, hybbx_bool_true_tokens, + sizeof(hybbx_bool_true_tokens) / + sizeof(hybbx_bool_true_tokens[0])); +} + +int hybbx_bool_is_false(const char *value) +{ + return bool_match_any(value, hybbx_bool_false_tokens, + sizeof(hybbx_bool_false_tokens) / + sizeof(hybbx_bool_false_tokens[0])); +} + +int hybbx_parse_bool(const char *value, int default_value) +{ + if (value == NULL || value[0] == '\0') { + return default_value; + } + + if (hybbx_bool_is_true(value)) { + return 1; + } + + if (hybbx_bool_is_false(value)) { + return 0; + } + + return default_value; +} + +const char *hybbx_bool_to_string(int value) +{ + return value ? HYBBX_BOOL_YES : HYBBX_BOOL_NO; +} + +const char *hybbx_result_name(hybbx_result_t rc) +{ + switch (rc) { + case HYBBX_OK: + return "ok"; + case HYBBX_LOCAL_CMD: + return "local_cmd"; + case HYBBX_SESSION_END: + return "session_end"; + case HYBBX_ERR_INVALID: + return "invalid"; + case HYBBX_ERR_NOMEM: + return "nomem"; + case HYBBX_ERR_NOT_FOUND: + return "not_found"; + case HYBBX_ERR_IO: + return "io"; + case HYBBX_ERR_UNSUPPORTED: + return "unsupported"; + case HYBBX_ERR_BUSY: + return "busy"; + case HYBBX_ERR_DENIED: + return "denied"; + default: + return "unknown"; + } +} + +size_t hybbx_strlcpy(char *dst, const char *src, size_t dst_size) +{ + size_t src_len; + + if (dst_size == 0) { + return src != NULL ? strlen(src) : 0; + } + + if (dst == NULL) { + return 0; + } + + if (src == NULL) { + dst[0] = '\0'; + return 0; + } + + src_len = strlen(src); + if (src_len >= dst_size) { + memcpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; + return dst_size - 1; + } + + memcpy(dst, src, src_len + 1); + return src_len; +} + +hybbx_result_t hybbx_default_user_data_path(char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_strlcpy(out, HYBBX_DIR_DATA, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static char g_install_root[HYBBX_PATH_MAX]; + +void hybbx_install_root_set(const char *root) +{ + if (root == NULL || root[0] == '\0') { + g_install_root[0] = '\0'; + return; + } + + hybbx_strlcpy(g_install_root, root, sizeof(g_install_root)); +} + +const char *hybbx_install_root_get(void) +{ + return g_install_root[0] != '\0' ? g_install_root : NULL; +} + +static int path_is_absolute(const char *path) +{ + if (path == NULL || path[0] == '\0') { + return 0; + } + + if (path[0] == '/') { + return 1; + } + +#if defined(_WIN32) || defined(__CYGWIN__) + if (((path[0] >= 'A' && path[0] <= 'Z') || + (path[0] >= 'a' && path[0] <= 'z')) && + path[1] == ':') { + return 1; + } +#endif + + return 0; +} + +hybbx_result_t hybbx_path_resolve(char *out, size_t out_len, const char *path) +{ + char expanded[HYBBX_PATH_MAX]; + const char *root; + hybbx_result_t rc; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_path_expand(expanded, sizeof(expanded), path); + if (rc != HYBBX_OK) { + return rc; + } + + if (path_is_absolute(expanded)) { + if (hybbx_strlcpy(out, expanded, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + root = hybbx_install_root_get(); + if (root != NULL && root[0] != '\0') { + return hybbx_path_join(out, out_len, root, expanded); + } + + if (hybbx_strlcpy(out, expanded, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_path_dirname(const char *path, char *out, size_t out_len) +{ + char copy[HYBBX_PATH_MAX]; + char *slash; + + if (path == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_strlcpy(copy, path, sizeof(copy)) >= sizeof(copy)) { + return HYBBX_ERR_INVALID; + } + + slash = strrchr(copy, '/'); + if (slash == NULL) { + if (hybbx_strlcpy(out, ".", out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (slash == copy) { + if (hybbx_strlcpy(out, "/", out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + *slash = '\0'; + if (hybbx_strlcpy(out, copy, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_platform_os_name(char *out, size_t out_len) +{ + const char *name; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + +#if defined(_WIN32) + name = "Windows"; +#elif defined(__AMIGA__) + name = "AmigaOS"; +#else + { + struct utsname u; + + if (uname(&u) != 0) { + return HYBBX_ERR_IO; + } + + name = u.sysname; + if (name == NULL || name[0] == '\0') { + return HYBBX_ERR_IO; + } + + if (strcmp(name, "Darwin") == 0) { + name = "MacOS"; + } + } +#endif + + if (hybbx_strlcpy(out, name, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_path_expand(char *out, size_t out_len, const char *path) +{ + const char *home; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (path == NULL || path[0] == '\0') { + return hybbx_default_user_data_path(out, out_len); + } + + if (path[0] != '~') { + if (hybbx_strlcpy(out, path, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (path[1] != '\0' && path[1] != '/') { + return HYBBX_ERR_INVALID; + } + + home = getenv("HOME"); + if (home == NULL || home[0] == '\0') { + if (path[1] == '\0') { + return hybbx_default_user_data_path(out, out_len); + } + if (hybbx_strlcpy(out, path + 2, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (path[1] == '\0') { + if (hybbx_strlcpy(out, home, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (snprintf(out, out_len, "%s%s", home, path + 1) >= (int)out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +int hybbx_size_ok(size_t len) +{ + return len > 0 && len <= HYBBX_ALLOC_MAX; +} + +static int path_component_valid(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return 0; + } + + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + return 0; + } + + if (strchr(name, '/') != NULL || strchr(name, '\\') != NULL) { + return 0; + } + + return 1; +} + +hybbx_result_t hybbx_path_join(char *out, size_t out_len, + const char *base, const char *name) +{ + size_t base_len; + int need_slash; + int written; + + if (out == NULL || out_len == 0 || base == NULL || name == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!path_component_valid(name)) { + return HYBBX_ERR_INVALID; + } + + base_len = strlen(base); + if (!hybbx_size_ok(base_len + 1) || !hybbx_size_ok(strlen(name) + 1)) { + return HYBBX_ERR_INVALID; + } + + need_slash = base_len > 0 && base[base_len - 1] != '/'; + written = snprintf(out, out_len, "%s%s%s", base, need_slash ? "/" : "", name); + if (written < 0 || (size_t)written >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static hybbx_time_format_t g_time_format; +static int g_time_format_ready; + +void hybbx_time_format_defaults(hybbx_time_format_t *fmt) +{ + if (fmt == NULL) { + return; + } + + fmt->clock_12h = 0; + fmt->seconds = 1; + fmt->date_format = HYBBX_DATE_ISO; +} + +const char *hybbx_date_format_name(hybbx_date_format_t fmt) +{ + switch (fmt) { + case HYBBX_DATE_ISO_SHORT: + return "iso_short"; + case HYBBX_DATE_US: + return "us"; + case HYBBX_DATE_EU: + return "eu"; + case HYBBX_DATE_ISO: + default: + return "iso"; + } +} + +#if !defined(HYBBX_CLIENT_BUILD) +static hybbx_date_format_t parse_date_format(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_DATE_ISO; + } + + if (bool_token_ieq(value, "iso") || bool_token_ieq(value, "yyyy_mm_dd") || + bool_token_ieq(value, "yyyy/mm/dd")) { + return HYBBX_DATE_ISO; + } + + if (bool_token_ieq(value, "iso_short") || bool_token_ieq(value, "yy_mm_dd") || + bool_token_ieq(value, "yy/mm/dd")) { + return HYBBX_DATE_ISO_SHORT; + } + + if (bool_token_ieq(value, "us") || bool_token_ieq(value, "mm_dd_yyyy") || + bool_token_ieq(value, "mm/dd/yyyy")) { + return HYBBX_DATE_US; + } + + if (bool_token_ieq(value, "eu") || bool_token_ieq(value, "dd_mm_yyyy") || + bool_token_ieq(value, "dd/mm/yyyy")) { + return HYBBX_DATE_EU; + } + + return HYBBX_DATE_ISO; +} +#endif + +hybbx_result_t hybbx_time_local_now(struct tm *out) +{ + time_t now; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + now = time(NULL); + if (now == (time_t)-1) { + return HYBBX_ERR_IO; + } + +#if defined(_WIN32) + if (localtime_s(out, &now) != 0) { + return HYBBX_ERR_IO; + } +#else + if (localtime_r(&now, out) == NULL) { + return HYBBX_ERR_IO; + } +#endif + + return HYBBX_OK; +} + +const hybbx_time_format_t *hybbx_time_format_get(void) +{ + if (!g_time_format_ready) { + hybbx_time_format_defaults(&g_time_format); + g_time_format_ready = 1; + } + + return &g_time_format; +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_time_config_apply(const struct hybbx_config *config) +{ + const char *clock; + int clock_12h; + + hybbx_time_format_defaults(&g_time_format); + + if (config != NULL) { + clock_12h = hybbx_config_get_bool(config, "time", "clock_12h", 0); + if (!clock_12h) { + clock_12h = hybbx_config_get_bool(config, "time", "am_pm", 0); + } + + clock = hybbx_config_get(config, "time", "clock", NULL); + if (clock != NULL && clock[0] != '\0') { + if (bool_token_ieq(clock, "12h") || bool_token_ieq(clock, "12") || + bool_token_ieq(clock, "am_pm") || bool_token_ieq(clock, "am/pm")) { + clock_12h = 1; + } else if (bool_token_ieq(clock, "24h") || bool_token_ieq(clock, "24")) { + clock_12h = 0; + } + } + + g_time_format.clock_12h = clock_12h; + g_time_format.seconds = + hybbx_config_get_bool(config, "time", "seconds", 1); + g_time_format.date_format = parse_date_format( + hybbx_config_get(config, "time", "date", NULL)); + } + + g_time_format_ready = 1; + + hybbx_log_info("[time] clock=%s seconds=%s date=%s", + g_time_format.clock_12h ? "12h" : "24h", + hybbx_bool_to_string(g_time_format.seconds), + hybbx_date_format_name(g_time_format.date_format)); +} +#else +void hybbx_time_config_apply(const struct hybbx_config *config) +{ + (void)config; + hybbx_time_format_defaults(&g_time_format); + g_time_format_ready = 1; +} +#endif + +hybbx_result_t hybbx_time_format_time(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int hour; + int minute; + int second; + const char *suffix; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + hour = tm->tm_hour; + minute = tm->tm_min; + second = tm->tm_sec; + suffix = ""; + + if (fmt->clock_12h) { + if (hour >= 12) { + suffix = "pm"; + if (hour > 12) { + hour -= 12; + } + } else { + suffix = "am"; + if (hour == 0) { + hour = 12; + } + } + } + + if (fmt->clock_12h) { + if (fmt->seconds) { + n = snprintf(out, out_len, "%02d:%02d:%02d %s", + hour, minute, second, suffix); + } else { + n = snprintf(out, out_len, "%02d:%02d %s", hour, minute, suffix); + } + } else if (fmt->seconds) { + n = snprintf(out, out_len, "%02d:%02d:%02d", hour, minute, second); + } else { + n = snprintf(out, out_len, "%02d:%02d", hour, minute); + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_time_format_date(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int year; + int month; + int day; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + + switch (fmt->date_format) { + case HYBBX_DATE_ISO_SHORT: + n = snprintf(out, out_len, "%02d/%02d/%02d", + year % 100, month, day); + break; + case HYBBX_DATE_US: + n = snprintf(out, out_len, "%02d/%02d/%04d", month, day, year); + break; + case HYBBX_DATE_EU: + n = snprintf(out, out_len, "%02d/%02d/%04d", day, month, year); + break; + case HYBBX_DATE_ISO: + default: + n = snprintf(out, out_len, "%04d/%02d/%02d", year, month, day); + break; + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_time_format_stamp(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int year; + int month; + int day; + int hour; + int minute; + int second; + const char *suffix; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + hour = tm->tm_hour; + minute = tm->tm_min; + second = tm->tm_sec; + suffix = ""; + + if (fmt->clock_12h) { + if (hour >= 12) { + suffix = "pm"; + if (hour > 12) { + hour -= 12; + } + } else { + suffix = "am"; + if (hour == 0) { + hour = 12; + } + } + } + + if (fmt->clock_12h) { + if (fmt->seconds) { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d:%02d %s", + year, month, day, hour, minute, second, suffix); + } else { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d %s", + year, month, day, hour, minute, suffix); + } + } else if (fmt->seconds) { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d:%02d", + year, month, day, hour, minute, second); + } else { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d", + year, month, day, hour, minute); + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +void hybbx_socket_nosigpipe(int fd) +{ +#ifdef SO_NOSIGPIPE + int on = 1; + + if (fd >= 0) { + (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); + } +#else + (void)fd; +#endif +} + +void hybbx_socket_log_bind_failure(const char *component, const char *addr, + unsigned port) +{ + int err = errno; + + if (component == NULL || addr == NULL) { + return; + } + +#if !defined(HYBBX_CLIENT_BUILD) + hybbx_log_warn("[%s] failed to bind %s:%u (%s)", component, addr, port, + strerror(err)); + if (err == EADDRINUSE) { + hybbx_log_warn("[%s] port %u already in use — another HyBBX instance may " + "already be running (ss -tlnp | grep %u)", + component, port, port); + } +#else + fprintf(stderr, "[%s] failed to bind %s:%u (%s)\n", component, addr, port, + strerror(err)); + if (err == EADDRINUSE) { + fprintf(stderr, + "[%s] port %u already in use — another HyBBX instance may " + "already be running (ss -tlnp | grep %u)\n", + component, port, port); + } +#endif +} + +hybbx_result_t hybbx_socket_peer_name(int fd, char *buf, size_t buf_len) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)fd; + (void)buf; + (void)buf_len; + return HYBBX_ERR_UNSUPPORTED; +#else + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + const void *ip; + char ip_buf[INET6_ADDRSTRLEN]; + + if (buf == NULL || buf_len == 0) { + return HYBBX_ERR_INVALID; + } + + buf[0] = '\0'; + + if (fd < 0) { + return HYBBX_ERR_INVALID; + } + + if (getpeername(fd, (struct sockaddr *)&addr, &addr_len) != 0) { + return HYBBX_ERR_IO; + } + + if (addr.ss_family == AF_INET) { + const struct sockaddr_in *in4 = (const struct sockaddr_in *)&addr; + + ip = &in4->sin_addr; + } else if (addr.ss_family == AF_INET6) { + const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&addr; + + ip = &in6->sin6_addr; + } else { + return HYBBX_ERR_UNSUPPORTED; + } + + if (inet_ntop(addr.ss_family, ip, ip_buf, sizeof(ip_buf)) == NULL) { + return HYBBX_ERR_IO; + } + + hybbx_strlcpy(buf, ip_buf, buf_len); + return HYBBX_OK; +#endif +} diff --git a/src/instance_role_main.c b/src/instance_role_main.c new file mode 100644 index 0000000..e61eb81 --- /dev/null +++ b/src/instance_role_main.c @@ -0,0 +1,7 @@ +#include "hybbx/instance.h" + +/* Linked only into hybbxd. */ +hybbx_instance_role_t hybbx_instance_role(void) +{ + return HYBBX_INSTANCE_MAIN; +} diff --git a/src/instance_role_proxy.c b/src/instance_role_proxy.c new file mode 100644 index 0000000..2f97b94 --- /dev/null +++ b/src/instance_role_proxy.c @@ -0,0 +1,7 @@ +#include "hybbx/instance.h" + +/* Linked only into hybbxpd. */ +hybbx_instance_role_t hybbx_instance_role(void) +{ + return HYBBX_INSTANCE_PROXY; +} diff --git a/src/instance_role_secondary.c b/src/instance_role_secondary.c new file mode 100644 index 0000000..b8a8704 --- /dev/null +++ b/src/instance_role_secondary.c @@ -0,0 +1,7 @@ +#include "hybbx/instance.h" + +/* Linked only into hybbxsd. */ +hybbx_instance_role_t hybbx_instance_role(void) +{ + return HYBBX_INSTANCE_SECONDARY; +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..670de7a --- /dev/null +++ b/src/main.c @@ -0,0 +1,371 @@ +/* + * HyBBX instance daemon — hybbxd (Main) / hybbxsd (Secondary) / hybbxpd (Proxy). + * Role is linked via instance_role_*.c. Plugins: CMake HYBBX_PLUGIN_*. + * Config: -c path/to/hybbx.ini + */ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/hybbx.h" +#include "hybbx/instance.h" +#include "hybbx/service.h" +#include "hybbx/registry.h" +#include "hybbx/config.h" +#include "hybbx/command.h" +#include "hybbx/commands_registry.h" +#include "hybbx/daemon_wrap.h" +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/instance.h" +#include "hybbx/privilege.h" + +#include +#include +#include +#include +#include + +#ifdef HYBBX_HAVE_PLUGIN_TELNET +extern const hybbx_transport_plugin_t hybbx_plugin_telnet; +#endif +#ifdef HYBBX_HAVE_PLUGIN_PACKET_RADIO +extern const hybbx_transport_plugin_t hybbx_plugin_packet_radio; +#endif +#ifdef HYBBX_HAVE_PLUGIN_ARDOP +extern const hybbx_transport_plugin_t hybbx_plugin_ardop; +#endif +#ifdef HYBBX_HAVE_PLUGIN_CRDOP +extern const hybbx_transport_plugin_t hybbx_plugin_crdop; +#endif +#ifdef HYBBX_HAVE_PLUGIN_SSH +extern const hybbx_transport_plugin_t hybbx_plugin_ssh; +#endif +#ifdef HYBBX_HAVE_PLUGIN_WEBSOCKET +extern const hybbx_transport_plugin_t hybbx_plugin_websocket; +#endif +#ifdef HYBBX_HAVE_PLUGIN_BAYCOM +extern const hybbx_transport_plugin_t hybbx_plugin_baycom; +#endif +#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY +extern const hybbx_transport_plugin_t hybbx_plugin_mains_proxy; +#endif + +static void register_builtin_plugins(void) +{ + /* Register every compiled plugin; instance/INI gating is at apply time. */ +#ifdef HYBBX_HAVE_PLUGIN_TELNET + hybbx_registry_register(&hybbx_plugin_telnet); +#endif +#ifdef HYBBX_HAVE_PLUGIN_PACKET_RADIO + hybbx_registry_register(&hybbx_plugin_packet_radio); +#endif +#ifdef HYBBX_HAVE_PLUGIN_ARDOP + hybbx_registry_register(&hybbx_plugin_ardop); +#endif +#ifdef HYBBX_HAVE_PLUGIN_CRDOP + hybbx_registry_register(&hybbx_plugin_crdop); +#endif +#ifdef HYBBX_HAVE_PLUGIN_SSH + hybbx_registry_register(&hybbx_plugin_ssh); +#endif +#ifdef HYBBX_HAVE_PLUGIN_WEBSOCKET + hybbx_registry_register(&hybbx_plugin_websocket); +#endif +#ifdef HYBBX_HAVE_PLUGIN_BAYCOM + hybbx_registry_register(&hybbx_plugin_baycom); +#endif +#ifdef HYBBX_HAVE_PLUGIN_MAINS_PROXY + hybbx_registry_register(&hybbx_plugin_mains_proxy); +#endif +} + +static void print_usage(const char *prog) +{ + fprintf(stderr, + "HyBBX %s — %s instance (%s)\n" + "\n" + "Usage: %s [options]\n" + "\n" + "Options:\n" + " -c, --config Load INI configuration file\n" + " -f, --foreground Run in foreground (default)\n" + " --screen [name] Run detached in GNU screen (default: %s)\n" + " --tmux [name] Run detached in tmux (default: %s)\n" + " --attach Attach to screen/tmux session (with --screen/--tmux)\n" + " -h, --help Show this help\n" + " -V, --version Show version\n" + "\n" + "Instances: hybbxd=Main hybbxsd=Secondary hybbxpd=Proxy\n" + "\n", + HYBBX_VERSION_STRING, + hybbx_instance_role_name(), + hybbx_instance_binary_name(), + prog, + hybbx_instance_binary_name(), + hybbx_instance_binary_name()); +} + +static void print_version(void) +{ + printf("HyBBX %s — %s (%s)%s\n", + HYBBX_VERSION_STRING, + hybbx_instance_role_name(), + hybbx_instance_binary_name(), + hybbx_instance_offers_user_bbx() ? "" : " [no user BBX]"); +} + +static void list_plugins_cb(const hybbx_transport_plugin_t *plugin, void *userdata) +{ + (void)userdata; + printf(" - %s (kind=%d, version=%u)\n", + plugin->name, (int)plugin->kind, plugin->version); +} + +static int path_is_readable(const char *path) +{ + return path != NULL && path[0] != '\0' && access(path, R_OK) == 0; +} + +/** + * When -c is omitted: HYBBX_CONFIG, then /hybbx.ini (install layout). + */ +static const char *discover_config_path(char *buf, size_t buflen, + const char *argv0) +{ + const char *env; + + if (buf == NULL || buflen == 0) { + return NULL; + } + + env = getenv("HYBBX_CONFIG"); + if (path_is_readable(env)) { + snprintf(buf, buflen, "%s", env); + return buf; + } + + if (argv0 == NULL || argv0[0] == '\0') { + return NULL; + } + +#if defined(__linux__) + { + char exe[HYBBX_PATH_MAX]; + char etc[HYBBX_PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + + if (n > 0) { + char *slash; + size_t dir_len; + + exe[n] = '\0'; + slash = strrchr(exe, '/'); + if (slash != NULL) { + dir_len = (size_t)(slash - exe); + if (dir_len + 1 < sizeof(etc)) { + memcpy(etc, exe, dir_len); + etc[dir_len] = '\0'; + if (hybbx_path_join(buf, buflen, etc, HYBBX_FILE_CONFIG) == + HYBBX_OK && + path_is_readable(buf)) { + return buf; + } + } + } + } + } +#endif + + snprintf(buf, buflen, "%s", argv0); + { + char path_copy[HYBBX_PATH_MAX]; + char candidate[HYBBX_PATH_MAX]; + char *dir; + + snprintf(path_copy, sizeof(path_copy), "%s", argv0); + dir = dirname(path_copy); + if (hybbx_path_join(candidate, sizeof(candidate), dir, + HYBBX_FILE_CONFIG) == HYBBX_OK && + path_is_readable(candidate)) { + snprintf(buf, buflen, "%s", candidate); + return buf; + } + } + + return NULL; +} + +static void setup_install_root(const char *config_path) +{ + const char *env; + + env = getenv(HYBBX_ENV_ROOT); + if (env != NULL && env[0] != '\0') { + hybbx_install_root_set(env); + return; + } + + if (config_path != NULL && config_path[0] != '\0') { + char dir[HYBBX_PATH_MAX]; + + if (hybbx_path_dirname(config_path, dir, sizeof(dir)) == HYBBX_OK) { + hybbx_install_root_set(dir); + } + } +} + +int main(int argc, char *argv[]) +{ + hybbx_service_t *service; + hybbx_config_t config; + const char *config_path = NULL; + char discovered_config[HYBBX_PATH_MAX]; + hybbx_daemon_launch_opts_t launch; + int have_config = 0; + int i; + hybbx_result_t rc; + + hybbx_daemon_launch_opts_defaults(&launch); + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return EXIT_SUCCESS; + } + if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) { + print_version(); + return EXIT_SUCCESS; + } + if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--foreground") == 0) { + launch.foreground = 1; + continue; + } + if (strcmp(argv[i], "--attach") == 0) { + launch.attach = 1; + continue; + } + if (strcmp(argv[i], "--screen") == 0) { + launch.use_screen = 1; + if (i + 1 < argc && argv[i + 1][0] != '-' && + strcmp(argv[i + 1], "-c") != 0 && + strcmp(argv[i + 1], "--config") != 0) { + hybbx_strlcpy(launch.session, argv[++i], sizeof(launch.session)); + } + continue; + } + if (strcmp(argv[i], "--tmux") == 0) { + launch.use_tmux = 1; + if (i + 1 < argc && argv[i + 1][0] != '-' && + strcmp(argv[i + 1], "-c") != 0 && + strcmp(argv[i + 1], "--config") != 0) { + hybbx_strlcpy(launch.session, argv[++i], sizeof(launch.session)); + } + continue; + } + if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "Missing argument for %s\n", argv[i]); + return EXIT_FAILURE; + } + config_path = argv[++i]; + continue; + } + + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(argv[0]); + return EXIT_FAILURE; + } + + rc = hybbx_daemon_apply_launch_opts(&launch, argv[0], argc, argv); + if (rc != HYBBX_OK) { + return EXIT_FAILURE; + } + + if (config_path == NULL) { + config_path = discover_config_path(discovered_config, + sizeof(discovered_config), argv[0]); + } + + register_builtin_plugins(); + + printf("HyBBX %s starting (%s / %s)\n", + HYBBX_VERSION_STRING, + hybbx_instance_binary_name(), + hybbx_instance_role_name()); + + service = hybbx_service_create(NULL); + if (service == NULL) { + fprintf(stderr, "Failed to create service (out of memory?)\n"); + return EXIT_FAILURE; + } + + if (config_path != NULL) { + rc = hybbx_config_load(&config, config_path); + + if (rc != HYBBX_OK) { + fprintf(stderr, "Failed to load config '%s'\n", config_path); + hybbx_service_destroy(service); + return EXIT_FAILURE; + } + + have_config = 1; + printf("Loaded configuration: %s\n", config_path); + + rc = hybbx_privilege_apply_from_config(&config); + if (rc != HYBBX_OK) { + fprintf(stderr, + "Privilege setup failed — %s\n", + hybbx_result_name(rc)); + hybbx_config_free(&config); + hybbx_service_destroy(service); + return EXIT_FAILURE; + } + + setup_install_root(config_path); + + rc = hybbx_commands_registry_init(); + if (rc != HYBBX_OK) { + fprintf(stderr, "Failed to load command registry\n"); + hybbx_config_free(&config); + hybbx_service_destroy(service); + return EXIT_FAILURE; + } + + rc = hybbx_service_apply_config(service, &config, config_path); + if (rc != HYBBX_OK) { + fprintf(stderr, + "Failed to apply configuration (storage path writable? " + "telnet/circuit bind ok?) — %s\n", + hybbx_result_name(rc)); + hybbx_commands_registry_shutdown(); + hybbx_config_free(&config); + hybbx_service_destroy(service); + return EXIT_FAILURE; + } + + printf("Available transport plugins:\n"); + hybbx_registry_foreach(list_plugins_cb, NULL); + } else { + fprintf(stderr, + "No configuration loaded. Use -c , set HYBBX_CONFIG, " + "or run hybbx-start.\n"); + fprintf(stderr, "No transports will listen without a config file.\n"); + } + + hybbx_service_set_launch_binary(service, argv[0]); + + hybbx_service_run(service); + + if (hybbx_service_shutdown_mode(service) == HYBBX_SHUTDOWN_RESTART) { + hybbx_service_restart_exec(service); + } + + if (have_config) { + hybbx_config_free(&config); + } + hybbx_commands_registry_shutdown(); + hybbx_service_destroy(service); + + return EXIT_SUCCESS; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..882aa94 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +add_executable(hybbx_test_util test_util.c) +target_link_libraries(hybbx_test_util PRIVATE hybbx_core) + +add_executable(hybbx_test_config test_config.c) +target_link_libraries(hybbx_test_config PRIVATE hybbx_core) + +add_executable(hybbx_test_mains_proxy test_mains_proxy.c) +target_link_libraries(hybbx_test_mains_proxy PRIVATE hybbx_core hybbx_plugin_mains_proxy hybbx_plugin_packet_radio) + +add_executable(hybbx_test_mains_proxy_e2e test_mains_proxy_e2e.c) +target_link_libraries(hybbx_test_mains_proxy_e2e PRIVATE hybbx_core hybbx_plugin_mains_proxy hybbx_plugin_packet_radio) + +add_executable(hybbx_test_storage_smoke test_storage_smoke.c) +target_link_libraries(hybbx_test_storage_smoke PRIVATE hybbx_core) + +function(hybbx_add_instance_test name role_src) + add_executable(${name} test_instance.c ${role_src}) + target_link_libraries(${name} PRIVATE hybbx_core) + add_test(NAME ${name} COMMAND ${name}) +endfunction() + +hybbx_add_instance_test(hybbx_test_instance_main + ${CMAKE_SOURCE_DIR}/src/instance_role_main.c) +hybbx_add_instance_test(hybbx_test_instance_secondary + ${CMAKE_SOURCE_DIR}/src/instance_role_secondary.c) +hybbx_add_instance_test(hybbx_test_instance_proxy + ${CMAKE_SOURCE_DIR}/src/instance_role_proxy.c) + +add_test(NAME util COMMAND hybbx_test_util) +add_test(NAME config COMMAND hybbx_test_config) +add_test(NAME mains_proxy COMMAND hybbx_test_mains_proxy) +add_test(NAME mains_proxy_e2e COMMAND hybbx_test_mains_proxy_e2e $) +add_test(NAME storage_smoke COMMAND hybbx_test_storage_smoke) diff --git a/tests/test_config.c b/tests/test_config.c new file mode 100644 index 0000000..006d6b5 --- /dev/null +++ b/tests/test_config.c @@ -0,0 +1,60 @@ +#include "hybbx/config.h" + +#include +#include +#include + +static unsigned g_failures; + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +int main(void) +{ + hybbx_config_t cfg; + const char *path = "hybbx_test_config.ini"; + FILE *fp; + + fp = fopen(path, "w"); + if (fp == NULL) { + fprintf(stderr, "FAIL fopen\n"); + return 1; + } + + fputs("[log]\n", fp); + fputs("dir = logs ; relative to install root\n", fp); + fputs("level = info # debug level\n", fp); + fputs("[quoted]\n", fp); + fputs("path = \"data;keep\" ; trailing comment\n", fp); + fclose(fp); + + if (hybbx_config_load(&cfg, path) != HYBBX_OK) { + fprintf(stderr, "FAIL config load\n"); + remove(path); + return 1; + } + + check_str("inline semicolon", hybbx_config_get(&cfg, "log", "dir", NULL), + "logs"); + check_str("inline hash", hybbx_config_get(&cfg, "log", "level", NULL), + "info"); + check_str("quoted semicolon", + hybbx_config_get(&cfg, "quoted", "path", NULL), "data;keep"); + + hybbx_config_free(&cfg); + remove(path); + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_config: ok"); + return 0; +} diff --git a/tests/test_instance.c b/tests/test_instance.c new file mode 100644 index 0000000..dd25cea --- /dev/null +++ b/tests/test_instance.c @@ -0,0 +1,170 @@ +#include "hybbx/instance.h" +#include "hybbx/networks.h" + +#include +#include +#include + +static int failures; + +static void expect_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + failures++; + } +} + +static void test_enforce_hub(void) +{ + hybbx_networks_config_t net; + + hybbx_networks_config_defaults(&net); + net.telnet = 1; + net.ax25 = 1; + net.baycom = 1; + net.ardop = 1; + net.crdop = 1; + net.websocket = 0; + net.mains_proxy = 0; + + if (hybbx_networks_enforce_instance(&net) != HYBBX_OK) { + fprintf(stderr, "FAIL enforce returned error\n"); + failures++; + return; + } + + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_MAIN: + expect_int("main ax25 off", net.ax25, 0); + expect_int("main baycom off", net.baycom, 0); + expect_int("main ardop off", net.ardop, 0); + expect_int("main crdop off", net.crdop, 0); + expect_int("main telnet off", net.telnet, 0); + expect_int("main websocket on", net.websocket, 1); + expect_int("main circuit on", net.circuit, 1); + break; + + case HYBBX_INSTANCE_SECONDARY: + expect_int("secondary ax25 kept", net.ax25, 1); + expect_int("secondary baycom kept", net.baycom, 1); + expect_int("secondary telnet off", net.telnet, 0); + expect_int("secondary websocket off", net.websocket, 0); + expect_int("secondary mains_proxy on", net.mains_proxy, 1); + break; + + case HYBBX_INSTANCE_PROXY: + expect_int("proxy ax25 kept", net.ax25, 1); + expect_int("proxy baycom kept", net.baycom, 1); + expect_int("proxy ardop kept", net.ardop, 1); + expect_int("proxy crdop kept", net.crdop, 1); + expect_int("proxy websocket off", net.websocket, 0); + expect_int("proxy circuit on", net.circuit, 1); + break; + + default: + fprintf(stderr, "FAIL unknown role\n"); + failures++; + break; + } +} + +static void test_plugin_allow_hub(void) +{ + switch (hybbx_instance_role()) { + case HYBBX_INSTANCE_MAIN: + expect_int("main packet_radio denied", + hybbx_instance_plugin_allowed("packet_radio"), 0); + expect_int("main baycom denied", + hybbx_instance_plugin_allowed("baycom"), 0); + expect_int("main ardop denied", + hybbx_instance_plugin_allowed("ardop"), 0); + expect_int("main crdop denied", + hybbx_instance_plugin_allowed("crdop"), 0); + expect_int("main websocket", hybbx_instance_plugin_allowed("websocket"), 1); + expect_int("main telnet", hybbx_instance_plugin_allowed("telnet"), 0); + expect_int("main user_bbx", hybbx_instance_offers_user_bbx(), 1); + break; + + case HYBBX_INSTANCE_SECONDARY: + expect_int("secondary packet_radio", + hybbx_instance_plugin_allowed("packet_radio"), 1); + expect_int("secondary baycom", + hybbx_instance_plugin_allowed("baycom"), 1); + expect_int("secondary mains_proxy", + hybbx_instance_plugin_allowed("mains_proxy"), 1); + expect_int("secondary telnet", + hybbx_instance_plugin_allowed("telnet"), 0); + expect_int("secondary websocket", + hybbx_instance_plugin_allowed("websocket"), 0); + expect_int("secondary user_bbx", hybbx_instance_offers_user_bbx(), 0); + break; + + case HYBBX_INSTANCE_PROXY: + expect_int("proxy packet_radio", + hybbx_instance_plugin_allowed("packet_radio"), 1); + expect_int("proxy baycom", + hybbx_instance_plugin_allowed("baycom"), 1); + expect_int("proxy telnet", hybbx_instance_plugin_allowed("telnet"), 1); + expect_int("proxy ssh", hybbx_instance_plugin_allowed("ssh"), 1); + expect_int("proxy websocket", + hybbx_instance_plugin_allowed("websocket"), 0); + expect_int("proxy mains_proxy", + hybbx_instance_plugin_allowed("mains_proxy"), 1); + expect_int("proxy user_bbx", hybbx_instance_offers_user_bbx(), 0); + break; + + default: + break; + } +} + +static void test_standalone_main(void) +{ + hybbx_networks_config_t net; + + if (hybbx_instance_role() != HYBBX_INSTANCE_MAIN) { + return; + } + + hybbx_instance_set_standalone(1); + + expect_int("standalone baycom allowed", + hybbx_instance_plugin_allowed("baycom"), 1); + expect_int("standalone packet_radio allowed", + hybbx_instance_plugin_allowed("packet_radio"), 1); + expect_int("standalone ardop allowed", + hybbx_instance_plugin_allowed("ardop"), 1); + + hybbx_networks_config_defaults(&net); + net.telnet = 1; + net.ax25 = 1; + net.baycom = 1; + net.ardop = 1; + net.crdop = 1; + + if (hybbx_networks_enforce_instance(&net) != HYBBX_OK) { + fprintf(stderr, "FAIL standalone enforce returned error\n"); + failures++; + hybbx_instance_set_standalone(0); + return; + } + + expect_int("standalone ax25 kept", net.ax25, 1); + expect_int("standalone baycom kept", net.baycom, 1); + expect_int("standalone ardop kept", net.ardop, 1); + expect_int("standalone crdop kept", net.crdop, 1); + + hybbx_instance_set_standalone(0); +} + +int main(void) +{ + printf("instance tests role=%s binary=%s\n", + hybbx_instance_role_name(), + hybbx_instance_binary_name()); + test_plugin_allow_hub(); + test_enforce_hub(); + test_standalone_main(); + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_mains_proxy.c b/tests/test_mains_proxy.c new file mode 100644 index 0000000..b95a904 --- /dev/null +++ b/tests/test_mains_proxy.c @@ -0,0 +1,189 @@ +#include "hybbx/mains_proxy.h" +#include "hybbx/util.h" + +#include +#include +#include + +static unsigned g_failures; + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +static void check_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + g_failures++; + } +} + +static void check_uint(const char *label, unsigned got, unsigned want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %u want %u\n", label, got, want); + g_failures++; + } +} + +static void parse_one_peer(const char *section, + hybbx_mains_proxy_peer_config_t *out) +{ + hybbx_result_t rc = hybbx_mains_proxy_peer_parse(section, out); + if (rc != HYBBX_OK) { + fprintf(stderr, "FAIL peer_parse returned %d for '%s'\n", (int)rc, section); + g_failures++; + } +} + +int main(void) +{ + hybbx_mains_proxy_peer_config_t peer; + const char *multi; + const char *sep; + char section_buf[1024]; + + /* 1. semicolon-separated section (serialized by hybbx_config_format_section) */ + parse_one_peer( + "peer_id=alpha;circuit_host=127.0.0.1;circuit_port=7323;" + "link_id=alpha-main;link_password=secret;enabled=yes;wire=circuit;" + "duplex=full;use_secondary=yes", + &peer); + check_str("semicolon peer_id", peer.peer_id, "alpha"); + check_str("semicolon circuit_host", peer.circuit_host, "127.0.0.1"); + check_uint("semicolon circuit_port", peer.circuit_port, 7323u); + check_str("semicolon link_id", peer.link_id, "alpha-main"); + check_str("semicolon link_password", peer.link_password, "secret"); + check_int("semicolon enabled", peer.enabled, 1); + check_int("semicolon wire", (int)peer.wire, (int)HYBBX_MAINS_PROXY_WIRE_CIRCUIT); + check_int("semicolon duplex", (int)peer.duplex, (int)HYBBX_MAINS_PROXY_DUPLEX_FULL); + check_int("semicolon use_secondary", peer.use_secondary, 1); + + /* 2. newline-separated section */ + parse_one_peer( + "peer_id=beta\ncircuit_host=127.0.0.2\ncircuit_port=7324\n" + "link_id=beta-main\nlink_password=other\nenabled=no\n" + "wire=ax25\nduplex=half\nuse_secondary=no", + &peer); + check_str("newline peer_id", peer.peer_id, "beta"); + check_str("newline circuit_host", peer.circuit_host, "127.0.0.2"); + check_uint("newline circuit_port", peer.circuit_port, 7324u); + check_str("newline link_id", peer.link_id, "beta-main"); + check_str("newline link_password", peer.link_password, "other"); + check_int("newline enabled", peer.enabled, 0); + check_int("newline wire", (int)peer.wire, (int)HYBBX_MAINS_PROXY_WIRE_AX25); + check_int("newline duplex", (int)peer.duplex, (int)HYBBX_MAINS_PROXY_DUPLEX_HALF); + check_int("newline use_secondary", peer.use_secondary, 0); + + /* 3. multiple peers separated by instance separator */ + multi = + "peer_id=first;circuit_host=127.0.0.1;enabled=yes\x1e" + "peer_id=second;circuit_host=127.0.0.2;enabled=yes\x1e" + "enabled=no\x1e" + "peer_id=fourth;circuit_host=127.0.0.4;enabled=yes"; + + sep = multi; + for (unsigned i = 0; i < 4; i++) { + const char *next = strchr(sep, HYBBX_MAINS_PROXY_INSTANCE_SEP); + size_t len; + + if (next != NULL) { + len = (size_t)(next - sep); + } else { + len = strlen(sep); + } + + if (len >= sizeof(section_buf)) { + len = sizeof(section_buf) - 1; + } + memcpy(section_buf, sep, len); + section_buf[len] = '\0'; + + parse_one_peer(section_buf, &peer); + + switch (i) { + case 0: + check_str("multi[0] peer_id", peer.peer_id, "first"); + check_str("multi[0] circuit_host", peer.circuit_host, "127.0.0.1"); + check_int("multi[0] enabled", peer.enabled, 1); + break; + case 1: + check_str("multi[1] peer_id", peer.peer_id, "second"); + check_str("multi[1] circuit_host", peer.circuit_host, "127.0.0.2"); + check_int("multi[1] enabled", peer.enabled, 1); + break; + case 2: + /* section with only enabled=no: disabled peer preserves defaults */ + check_int("multi[2] enabled", peer.enabled, 0); + check_str("multi[2] peer_id", peer.peer_id, ""); + check_str("multi[2] circuit_host", peer.circuit_host, ""); + break; + case 3: + check_str("multi[3] peer_id", peer.peer_id, "fourth"); + check_str("multi[3] circuit_host", peer.circuit_host, "127.0.0.4"); + check_int("multi[3] enabled", peer.enabled, 1); + break; + } + + if (next == NULL) { + break; + } + sep = next + 1; + } + + /* 4. missing credentials: empty peer_id, empty link_password, invalid port */ + parse_one_peer( + "circuit_host=127.0.0.1;circuit_port=99999;link_id=main;enabled=yes", + &peer); + check_str("missing peer_id", peer.peer_id, ""); + check_str("missing link_password", peer.link_password, ""); + /* invalid port should fall back to default */ + check_uint("invalid port default", peer.circuit_port, + (unsigned)HYBBX_CIRCUIT_DEFAULT_PORT); + + /* 5. legacy host/port keys: host maps to circuit_host, port is ignored */ + parse_one_peer( + "peer_id=legacy;circuit_host=127.0.0.1;host=127.0.0.9;port=9999;" + "link_id=main;link_password=secret;enabled=yes", + &peer); + check_str("legacy host ignored", peer.circuit_host, "127.0.0.1"); + check_uint("legacy port ignored", peer.port, 9999u); + + /* 6. empty section should yield defaults */ + parse_one_peer("", &peer); + check_str("empty peer_id", peer.peer_id, ""); + check_int("empty enabled default", peer.enabled, 1); + check_int("empty wire default", (int)peer.wire, + (int)HYBBX_MAINS_PROXY_WIRE_CIRCUIT); + check_int("empty duplex default", (int)peer.duplex, + (int)HYBBX_MAINS_PROXY_DUPLEX_FULL); + check_int("empty use_secondary default", peer.use_secondary, 1); + + + /* 7. mesh active: stopped mesh reports inactive */ + hybbx_mains_proxy_mesh_stop(NULL); + check_int("mesh_active when stopped", hybbx_mains_proxy_mesh_active(), 0); + + /* 8. peer parse leaves legacy host intact when circuit_host is set */ + parse_one_peer( + "peer_id=explicit;circuit_host=127.0.0.1;host=127.0.0.9;" + "link_id=main;link_password=secret;enabled=yes", + &peer); + check_str("circuit_host wins over host", peer.circuit_host, "127.0.0.1"); + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_mains_proxy: ok"); + return 0; +} + +/* Test kept separate above; this file ends after main() in the original. */ diff --git a/tests/test_mains_proxy_e2e.c b/tests/test_mains_proxy_e2e.c new file mode 100644 index 0000000..bb93f0c --- /dev/null +++ b/tests/test_mains_proxy_e2e.c @@ -0,0 +1,360 @@ +#include "hybbx/mains_proxy.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/util.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static unsigned g_failures; +static pid_t g_pid_a = -1; +static pid_t g_pid_b = -1; + +static void check_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + g_failures++; + } +} + +static int write_file(const char *path, const char *text) +{ + FILE *fp = fopen(path, "w"); + if (fp == NULL) { + return -1; + } + if (fputs(text, fp) == EOF) { + fclose(fp); + return -1; + } + fclose(fp); + return 0; +} + +static int mkdir_p(const char *path) +{ + char tmp[512]; + char *p; + size_t len; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + hybbx_strlcpy(tmp, path, sizeof(tmp)); + len = strlen(tmp); + if (tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + + for (p = tmp + 1; *p != '\0'; p++) { + if (*p == '/') { + *p = '\0'; + (void)mkdir(tmp, 0755); + *p = '/'; + } + } + (void)mkdir(tmp, 0755); + return 0; +} + +static int copy_file(const char *src, const char *dst) +{ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "cp '%s' '%s'", src, dst); + return system(cmd); +} + +static int copy_dir(const char *src, const char *dst) +{ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "cp -r '%s' '%s'", src, dst); + return system(cmd); +} + +static void write_main_ini(const char *root, const char *name, + unsigned circuit_port, unsigned peer_port, + const char *local_link_id, + const char *remote_link_id, + unsigned websocket_port) +{ + char path[512]; + char text[2048]; + + snprintf(path, sizeof(path), "%s/data", root); + mkdir_p(path); + snprintf(path, sizeof(path), "%s/logs", root); + mkdir_p(path); + snprintf(path, sizeof(path), "%s/mail", root); + mkdir_p(path); + copy_file("/home/akb/Code2/10-PROJECTS/hyBBX/share/commands.yaml", root); + copy_file("/home/akb/Code2/10-PROJECTS/hyBBX/share/areas.yaml", root); + copy_dir("/home/akb/Code2/10-PROJECTS/hyBBX/text", root); + + snprintf(path, sizeof(path), "%s/hybbx.ini", root); + snprintf(text, sizeof(text), + "[service]\n" + "name = %s\n" + "login_announce = no\n" + "\n" + "[storage]\n" + "backend = flatfile\n" + "path = data\n" + "\n" + "[auth]\n" + "auto_login = yes\n" + "\n" + "[log]\n" + "enabled = yes\n" + "dir = logs\n" + "level = info\n" + "\n" + "[security]\n" + "enabled = yes\n" + "telnet = no\n" + "ssh = no\n" + "websocket = no\n" + "circuit = yes\n" + "\n" + "[mail]\n" + "enabled = yes\n" + "path = mail\n" + "\n" + "[networks]\n" + "telnet = no\n" + "ssh = no\n" + "websocket = no\n" + "ax25 = no\n" + "baycom = no\n" + "tmodem = no\n" + "ardop = no\n" + "crdop = no\n" + "circuit = yes\n" + "mains_proxy = yes\n" + "\n" + "[circuit]\n" + "enabled = yes\n" + "bind = 127.0.0.1\n" + "port = %u\n" + "link_auth = yes\n" + "link_password = secret\n" + "max_links = 8\n" + "balance = yes\n" + "\n" + "[transport.websocket]\n" + "bind = 127.0.0.1\n" + "port = %u\n" + "\n" + "[transport.mains_proxy1]\n" + "peer_id = peer\n" + "circuit_host = 127.0.0.1\n" + "circuit_port = %u\n" + "link_id = %s\n" + "link_password = secret\n" + "wire = circuit\n" + "duplex = full\n" + "\n" + "[transport.mains_proxy2]\n" + "peer_id = peer\n" + "link_id = %s\n" + "link_password = secret\n", + name, circuit_port, websocket_port, peer_port, + local_link_id, remote_link_id); + + if (write_file(path, text) != 0) { + fprintf(stderr, "FAIL cannot write %s\n", path); + g_failures++; + } +} + +static pid_t start_hybbxd(const char *binary, const char *root) +{ + pid_t pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + char config_path[512]; + int null_fd = open("/dev/null", O_RDONLY); + int log_fd; + char log_path[512]; + + snprintf(log_path, sizeof(log_path), "%s/logs/test-stderr.log", root); + log_fd = open(log_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + + setsid(); + if (null_fd >= 0) { + dup2(null_fd, STDIN_FILENO); + close(null_fd); + } + if (log_fd >= 0) { + dup2(log_fd, STDOUT_FILENO); + dup2(log_fd, STDERR_FILENO); + close(log_fd); + } + + snprintf(config_path, sizeof(config_path), "%s/hybbx.ini", root); + setenv("HYBBX_ROOT", root, 1); + execl(binary, binary, "-c", config_path, "-f", (char *)NULL); + _exit(127); + } + return pid; +} + +static void stop_process(pid_t pid) +{ + if (pid > 0) { + kill(pid, SIGTERM); + waitpid(pid, NULL, 0); + } +} + +static void cleanup_dirs(const char *a, const char *b) +{ + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "rm -rf '%s' '%s'", a, b); + (void)system(cmd); +} + +static int log_contains(const char *root, const char *needle) +{ + char path[512]; + char buf[4096]; + FILE *fp; + size_t n; + + snprintf(path, sizeof(path), "%s/logs/test-stderr.log", root); + fp = fopen(path, "r"); + if (fp == NULL) { + return 0; + } + + while ((n = fread(buf, 1, sizeof(buf) - 1, fp)) > 0) { + buf[n] = '\0'; + if (strstr(buf, needle) != NULL) { + fclose(fp); + return 1; + } + } + fclose(fp); + return 0; +} + +static int wait_for_port(unsigned port, unsigned timeout_ms) +{ + unsigned elapsed = 0; + int fd; + struct sockaddr_in addr; + + while (elapsed < timeout_ms) { + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) { + close(fd); + return 0; + } + close(fd); + usleep(100000); + elapsed += 100; + } + return -1; +} + +int main(int argc, char **argv) +{ + const char *binary = (argc > 1) ? argv[1] : "/tmp/hybbx-p01-build/src/hybbxd"; + char root_a[256]; + char root_b[256]; + int active; + unsigned elapsed; + + snprintf(root_a, sizeof(root_a), "/tmp/hybbx-e2e-a-%d", (int)getpid()); + snprintf(root_b, sizeof(root_b), "/tmp/hybbx-e2e-b-%d", (int)getpid()); + + cleanup_dirs(root_a, root_b); + + /* Main A connects outbound as main-a; Main B accepts id=main-a. */ + write_main_ini(root_a, "main-a", 17323, 17324, "main-a", "main-b", 14591); + /* Main B connects outbound as main-b; Main A accepts id=main-b. */ + write_main_ini(root_b, "main-b", 17324, 17323, "main-b", "main-a", 14592); + + g_pid_a = start_hybbxd(binary, root_a); + g_pid_b = start_hybbxd(binary, root_b); + + check_int("start main-a", g_pid_a > 0, 1); + check_int("start main-b", g_pid_b > 0, 1); + + if (g_pid_a <= 0 || g_pid_b <= 0) { + stop_process(g_pid_b); + cleanup_dirs(root_a, root_b); + return 1; + } + + /* Wait for both circuit hubs to accept connections. */ + check_int("port a ready", wait_for_port(17323, 5000), 0); + check_int("port b ready", wait_for_port(17324, 5000), 0); + + /* Wait for mains_proxy to establish outbound links on each Main. */ + { + int a_linked = 0; + int b_linked = 0; + elapsed = 0; + while (elapsed < 10000) { + a_linked = log_contains(root_a, "[mains_proxy] linked to peer"); + b_linked = log_contains(root_b, "[mains_proxy] linked to peer"); + if (a_linked && b_linked) { + break; + } + usleep(100000); + elapsed += 100; + } + check_int("main-a linked via mains_proxy", a_linked, 1); + check_int("main-b linked via mains_proxy", b_linked, 1); + } + + /* P0.2: verify both Mains accepted the peer link via HBX auth. */ + { + int a_has_main_b = log_contains(root_a, "link authenticated id=main-b"); + int b_has_main_a = log_contains(root_b, "link authenticated id=main-a"); + check_int("main-a sees main-b authenticated", a_has_main_b, 1); + check_int("main-b sees main-a authenticated", b_has_main_a, 1); + } + + + stop_process(g_pid_a); + stop_process(g_pid_b); + + if (g_failures == 0) { + cleanup_dirs(root_a, root_b); + } else { + fprintf(stderr, " left dirs: %s %s\n", root_a, root_b); + } + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_mains_proxy_e2e: ok"); + return 0; +} diff --git a/tests/test_storage_smoke.c b/tests/test_storage_smoke.c new file mode 100644 index 0000000..77fd3cf --- /dev/null +++ b/tests/test_storage_smoke.c @@ -0,0 +1,235 @@ +/* + * test_storage_smoke.c — P0.4: flatfile + SQLite core smoke + * + * Tests: open/close, register_user, find_user, resolve_user, + * count_level, update_user, delete_user, session_begin/end. + * Runs against both flatfile and SQLite backends in temporary directories. + */ +#include "hybbx/storage.h" +#include "hybbx/auth.h" +#include "hybbx/util.h" + +#include +#include +#include +#include +#include + +static unsigned g_failures; + +static void check_rc(const char *label, hybbx_result_t got, hybbx_result_t want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, (int)got, (int)want); + g_failures++; + } +} + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +static void check_uint64(const char *label, uint64_t got, uint64_t want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %llu want %llu\n", + label, (unsigned long long)got, (unsigned long long)want); + g_failures++; + } +} + +static void check_nonzero(const char *label, uint64_t val) +{ + if (val == 0) { + fprintf(stderr, "FAIL %s: expected non-zero\n", label); + g_failures++; + } +} + +static void mkdir_p(const char *path) +{ + char tmp[512]; + char *p; + size_t len; + + hybbx_strlcpy(tmp, path, sizeof(tmp)); + len = strlen(tmp); + if (len > 0 && tmp[len - 1] == '/') { + tmp[len - 1] = '\0'; + } + for (p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } + mkdir(tmp, 0755); +} + +static void rm_rf(const char *path) +{ + char cmd[512]; + snprintf(cmd, sizeof(cmd), "rm -rf '%s'", path); + (void)system(cmd); +} + +static hybbx_storage_t *open_backend(hybbx_storage_backend_kind_t backend, + const char *path) +{ + hybbx_storage_options_t opts; + + memset(&opts, 0, sizeof(opts)); + opts.backend = backend; + opts.path = path; + return hybbx_storage_open(&opts); +} + +/* + * Run the full CRUD + session smoke against one backend. + */ +static void smoke_backend(hybbx_storage_backend_kind_t backend, + const char *label, + const char *path) +{ + hybbx_storage_t *st; + hybbx_user_registration_t reg; + hybbx_user_record_t user; + hybbx_user_record_t found; + hybbx_session_record_t sess; + size_t count; + char test_user[64]; + + snprintf(test_user, sizeof(test_user), "smk_%s", label); + + rm_rf(path); + mkdir_p(path); + + st = open_backend(backend, path); + if (st == NULL) { + fprintf(stderr, "FAIL %s: storage_open returned NULL\n", label); + g_failures++; + return; + } + + check_uint64("backend kind", + (uint64_t)hybbx_storage_backend(st), (uint64_t)backend); + + /* register_user — level defaults to HYBBX_LEVEL_USER */ + memset(®, 0, sizeof(reg)); + hybbx_strlcpy(reg.username, test_user, sizeof(reg.username)); + hybbx_strlcpy(reg.password, "testpass123", sizeof(reg.password)); + hybbx_strlcpy(reg.nickname, test_user, sizeof(reg.nickname)); + hybbx_strlcpy(reg.full_name, "Smoke Test", sizeof(reg.full_name)); + hybbx_strlcpy(reg.country, "DE", sizeof(reg.country)); + hybbx_strlcpy(reg.location, "localhost", sizeof(reg.location)); + hybbx_strlcpy(reg.email, "smoke@test.local", sizeof(reg.email)); + + check_rc("register_user", + hybbx_storage_register_user(st, ®, &user), HYBBX_OK); + check_nonzero("user id", user.id); + check_str("user username", user.username, test_user); + check_uint64("user level", (uint64_t)user.level, + (uint64_t)HYBBX_LEVEL_USER); + + /* find_user */ + memset(&found, 0, sizeof(found)); + check_rc("find_user", + hybbx_storage_find_user(st, test_user, &found), HYBBX_OK); + check_uint64("found id matches", found.id, user.id); + check_str("found username", found.username, test_user); + + /* find_user nonexistent */ + check_rc("find_user missing", + hybbx_storage_find_user(st, "no_such_user", &found), + HYBBX_ERR_NOT_FOUND); + + /* resolve_user (by name) */ + memset(&found, 0, sizeof(found)); + check_rc("resolve_user", + hybbx_storage_resolve_user(st, test_user, &found), HYBBX_OK); + check_uint64("resolve id matches", found.id, user.id); + + /* count_level */ + count = 0; + check_rc("count_level", + hybbx_storage_count_level(st, HYBBX_LEVEL_USER, &count), + HYBBX_OK); + /* count >= 1 (our user) */ + + /* update_user */ + user.level = HYBBX_LEVEL_SYSOP; + check_rc("update_user", + hybbx_storage_update_user(st, &user), HYBBX_OK); + + memset(&found, 0, sizeof(found)); + check_rc("find after update", + hybbx_storage_find_user(st, test_user, &found), HYBBX_OK); + check_uint64("updated level", (uint64_t)found.level, + (uint64_t)HYBBX_LEVEL_SYSOP); + + /* session_begin */ + memset(&sess, 0, sizeof(sess)); + check_rc("session_begin", + hybbx_storage_session_begin(st, &user, "test", &sess), HYBBX_OK); + check_nonzero("session id", sess.session_id); + + /* session_end */ + check_rc("session_end", + hybbx_storage_session_end(st, sess.session_id), HYBBX_OK); + + /* delete_user */ + check_rc("delete_user", + hybbx_storage_delete_user(st, user.id), HYBBX_OK); + + /* find after delete */ + check_rc("find after delete", + hybbx_storage_find_user(st, test_user, &found), + HYBBX_ERR_NOT_FOUND); + + /* Persistence: close and reopen, verify functional */ + hybbx_storage_close(st); + st = open_backend(backend, path); + if (st == NULL) { + fprintf(stderr, "FAIL %s: reopen returned NULL\n", label); + g_failures++; + return; + } + + /* Re-register after reopen to verify backend is functional */ + check_rc("re-register after reopen", + hybbx_storage_register_user(st, ®, &user), HYBBX_OK); + check_nonzero("re-registered id", user.id); + + hybbx_storage_delete_user(st, user.id); + hybbx_storage_close(st); + + rm_rf(path); +} + +int main(void) +{ + char path_ff[256]; + char path_sql[256]; + + snprintf(path_ff, sizeof(path_ff), "/tmp/hybbx-smoke-flatfile-%d", + (int)getpid()); + snprintf(path_sql, sizeof(path_sql), "/tmp/hybbx-smoke-sqlite-%d", + (int)getpid()); + + smoke_backend(HYBBX_STORAGE_FLATFILE, "flatfile", path_ff); + smoke_backend(HYBBX_STORAGE_SQLITE, "sqlite", path_sql); + + if (g_failures != 0) { + fprintf(stderr, "%u failure(s)\n", g_failures); + return 1; + } + + puts("test_storage_smoke: ok"); + return 0; +} diff --git a/tests/test_util.c b/tests/test_util.c new file mode 100644 index 0000000..cbd4e1b --- /dev/null +++ b/tests/test_util.c @@ -0,0 +1,152 @@ +#include "hybbx/limits.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include +#include +#include + +static unsigned g_failures; + +static void check_int(const char *label, int got, int want) +{ + if (got != want) { + fprintf(stderr, "FAIL %s: got %d want %d\n", label, got, want); + g_failures++; + } +} + +static void check_str(const char *label, const char *got, const char *want) +{ + if (got == NULL || want == NULL || strcmp(got, want) != 0) { + fprintf(stderr, "FAIL %s: got '%s' want '%s'\n", + label, got != NULL ? got : "(null)", want); + g_failures++; + } +} + +int main(void) +{ + check_int("true yes", hybbx_bool_is_true("yes"), 1); + check_int("true enable", hybbx_bool_is_true("enable"), 1); + check_int("false no", hybbx_bool_is_false("no"), 1); + check_int("false disable", hybbx_bool_is_false("disable"), 1); + + check_int("parse yes", hybbx_parse_bool("yes", 0), 1); + check_int("parse false", hybbx_parse_bool("false", 1), 0); + check_int("parse empty", hybbx_parse_bool("", 1), 1); + check_int("parse garbage", hybbx_parse_bool("maybe", 0), 0); + + check_str("bool yes", hybbx_bool_to_string(1), HYBBX_BOOL_YES); + check_str("bool no", hybbx_bool_to_string(0), HYBBX_BOOL_NO); + + check_str("result ok", hybbx_result_name(HYBBX_OK), "ok"); + check_str("result invalid", hybbx_result_name(HYBBX_ERR_INVALID), "invalid"); + check_str("result unknown", hybbx_result_name((hybbx_result_t)-99), "unknown"); + + check_int("log parse debug", (int)hybbx_log_parse_level("debug"), + (int)HYBBX_LOG_DEBUG); + check_int("log parse stats", (int)hybbx_log_parse_level("stats"), + (int)HYBBX_LOG_STATS); + check_int("log parse info", (int)hybbx_log_parse_level("info"), + (int)HYBBX_LOG_INFO); + check_int("log parse warn", (int)hybbx_log_parse_level("warn"), + (int)HYBBX_LOG_WARN); + check_int("log parse unknown", (int)hybbx_log_parse_level("verbose"), + (int)HYBBX_LOG_WARN); + check_str("log name stats", hybbx_log_level_name(HYBBX_LOG_STATS), "stats"); + + { + char path[HYBBX_PATH_MAX]; + const char *home = getenv("HOME"); + + if (hybbx_path_expand(path, sizeof(path), NULL) == HYBBX_OK) { + check_str("path expand null", path, HYBBX_DIR_DATA); + } + if (home != NULL && home[0] != '\0') { + char want[HYBBX_PATH_MAX]; + + snprintf(want, sizeof(want), "%s/custom", home); + if (hybbx_path_expand(path, sizeof(path), "~/custom") == HYBBX_OK) { + check_str("path expand tilde", path, want); + } + if (hybbx_path_expand(path, sizeof(path), "~") == HYBBX_OK) { + check_str("path expand home only", path, home); + } + } + } + + { + char path[HYBBX_PATH_MAX]; + char want[HYBBX_PATH_MAX]; + + hybbx_install_root_set("/opt/hybbx"); + if (hybbx_path_resolve(path, sizeof(path), HYBBX_DIR_DATA) == HYBBX_OK) { + snprintf(want, sizeof(want), "/opt/hybbx/%s", HYBBX_DIR_DATA); + check_str("path resolve data", path, want); + } + if (hybbx_path_resolve(path, sizeof(path), HYBBX_DIR_LOGS) == HYBBX_OK) { + snprintf(want, sizeof(want), "/opt/hybbx/%s", HYBBX_DIR_LOGS); + check_str("path resolve logs", path, want); + } + hybbx_install_root_set(NULL); + } + + { + char os_name[HYBBX_PATH_MAX]; + + if (hybbx_platform_os_name(os_name, sizeof(os_name)) != HYBBX_OK) { + fprintf(stderr, "FAIL platform os name: lookup failed\n"); + g_failures++; + } else if (os_name[0] == '\0') { + fprintf(stderr, "FAIL platform os name: empty\n"); + g_failures++; +#if defined(__linux__) + } else if (strcmp(os_name, "Linux") != 0) { + fprintf(stderr, "FAIL platform os name: got '%s' want 'Linux'\n", + os_name); + g_failures++; +#endif + } + } + + { + struct tm tm_ref = {0}; + char time_buf[32]; + char date_buf[32]; + hybbx_time_format_t fmt; + + tm_ref.tm_year = 126; + tm_ref.tm_mon = 6; + tm_ref.tm_mday = 8; + tm_ref.tm_hour = 15; + tm_ref.tm_min = 4; + tm_ref.tm_sec = 5; + + hybbx_time_format_defaults(&fmt); + if (hybbx_time_format_time(time_buf, sizeof(time_buf), &tm_ref, + &fmt) == HYBBX_OK) { + check_str("time 24h seconds", time_buf, "15:04:05"); + } + + fmt.date_format = HYBBX_DATE_US; + if (hybbx_time_format_date(date_buf, sizeof(date_buf), &tm_ref, + &fmt) == HYBBX_OK) { + check_str("date us", date_buf, "07/08/2026"); + } + + fmt.date_format = HYBBX_DATE_ISO; + if (hybbx_time_format_date(date_buf, sizeof(date_buf), &tm_ref, + &fmt) == HYBBX_OK) { + check_str("date iso", date_buf, "2026/07/08"); + } + } + + if (g_failures != 0) { + fprintf(stderr, "%u test(s) failed\n", g_failures); + return EXIT_FAILURE; + } + + puts("test_util: ok"); + return EXIT_SUCCESS; +} diff --git a/text/banner.txt b/text/banner.txt new file mode 100644 index 0000000..d86bc15 --- /dev/null +++ b/text/banner.txt @@ -0,0 +1,4 @@ +@service@ +@version@ %date% %time% + +/help /login /register diff --git a/text/motd.txt b/text/motd.txt new file mode 100644 index 0000000..7e1151c --- /dev/null +++ b/text/motd.txt @@ -0,0 +1,5 @@ +Hello, @username@. + +%date% %time% + +/news /who /msg diff --git a/text/news.txt b/text/news.txt new file mode 100644 index 0000000..9823220 --- /dev/null +++ b/text/news.txt @@ -0,0 +1 @@ +HyBBX 2.4.1 — Menu/index: General command list wraps at 80 columns (/session /version visible). BayCom: optional transport plugin (CMake ON; example INI `baycom=no`). MAX25 attach: `kiss_entry=none`, `[max25] check=yes`. Includes v2.0.0-p2 baseline (menu registry, TNC recovery, broadcast fixes). diff --git a/text/rules.txt b/text/rules.txt new file mode 100644 index 0000000..837efbc --- /dev/null +++ b/text/rules.txt @@ -0,0 +1,50 @@ +HyBBX — Terms of use (v2.4.0) + +HyBBX is a session service for low-bandwidth links: mail, chat, +conference, and commands over telnet, SSH, or WebSocket. RF paths +(packet radio, ARDOP, and similar) connect via HBX circuits; on-air use +depends on your site and licence. + +1. Your responsibility +Access is at your own risk. You answer for what you send, post, and do +on this system and on the air. The operator is not responsible for how +users or link partners behave. + +2. Identity +Real names are not required. A nickname or amateur callsign is enough. +For RF and circuit links, your CALL or link identity (CALLID) must be +truthful where your licence or local rules require it. Account data used +for administration may be seen by Sysop or Admin only. + +3. Data +User data is not sold. Mail and accounts remain on this host unless +disclosure is required by law. + +4. Logging and security +Sessions and security events may be recorded (for example in +security.log) to run the service, fix faults, and limit abuse. + +HyBBX may temporarily block an IP address, a callsign, or a circuit +link identity (CALLID) after repeated login failures, link-auth +failures, connection flooding, or other clear abuse. Blocks are short +cool-downs by default, not permanent bans. Normal chat and mail traffic +is only paced or size-limited — busy use on a slow link is expected and +does not by itself trigger a block. + +5. Spam and traffic limits +Message length, mail quotas, output pacing, and similar limits keep the +system usable on slow links. They are not punishment. Only repeated, +excessive spam or misuse may lead to a security block as in section 4. + +6. Acceptable use +Do not break the law. Do not engage in non-social behaviour: threats, +harassment, hate, or deliberate disruption of others. Do not attempt +to break into accounts, flood logins, or damage the system or its users. +Unlawful content or conduct may be reported to authorities where required. + +Sysop, Admin, and automated security may limit, block, or revoke access without +prior notice when abuse is evident. + +7. Changes +The operator may revise this notice in text/rules.txt. Read the current +text anytime with /rules or /legal. diff --git a/text/version.txt b/text/version.txt new file mode 100644 index 0000000..09a2c98 --- /dev/null +++ b/text/version.txt @@ -0,0 +1,2 @@ +HyBBX @version@ +Operating system: @os@ diff --git a/third_party/monocypher/monocypher.c b/third_party/monocypher/monocypher.c new file mode 100644 index 0000000..6e323a8 --- /dev/null +++ b/third_party/monocypher/monocypher.c @@ -0,0 +1,2956 @@ +// Monocypher version __git__ +// +// This file is dual-licensed. Choose whichever licence you want from +// the two licences listed below. +// +// The first licence is a regular 2-clause BSD licence. The second licence +// is the CC-0 from Creative Commons. It is intended to release Monocypher +// to the public domain. The BSD licence serves as a fallback option. +// +// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0 +// +// ------------------------------------------------------------------------ +// +// Copyright (c) 2017-2020, Loup Vaillant +// All rights reserved. +// +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ------------------------------------------------------------------------ +// +// Written in 2017-2020 by Loup Vaillant +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// + +#include "monocypher.h" + +#ifdef MONOCYPHER_CPP_NAMESPACE +namespace MONOCYPHER_CPP_NAMESPACE { +#endif + +///////////////// +/// Utilities /// +///////////////// +#define FOR_T(type, i, start, end) for (type i = (start); i < (end); i++) +#define FOR(i, start, end) FOR_T(size_t, i, start, end) +#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_] +#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0 +#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx))) +#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer)) +#define MIN(a, b) ((a) <= (b) ? (a) : (b)) +#define MAX(a, b) ((a) >= (b) ? (a) : (b)) + +typedef int8_t i8; +typedef uint8_t u8; +typedef int16_t i16; +typedef uint32_t u32; +typedef int32_t i32; +typedef int64_t i64; +typedef uint64_t u64; + +static const u8 zero[128] = {0}; + +// returns the smallest positive integer y such that +// (x + y) % pow_2 == 0 +// Basically, y is the "gap" missing to align x. +// Only works when pow_2 is a power of 2. +// Note: we use ~x+1 instead of -x to avoid compiler warnings +static size_t gap(size_t x, size_t pow_2) +{ + return (~x + 1) & (pow_2 - 1); +} + +static u32 load24_le(const u8 s[3]) +{ + return + ((u32)s[0] << 0) | + ((u32)s[1] << 8) | + ((u32)s[2] << 16); +} + +static u32 load32_le(const u8 s[4]) +{ + return + ((u32)s[0] << 0) | + ((u32)s[1] << 8) | + ((u32)s[2] << 16) | + ((u32)s[3] << 24); +} + +static u64 load64_le(const u8 s[8]) +{ + return load32_le(s) | ((u64)load32_le(s+4) << 32); +} + +static void store32_le(u8 out[4], u32 in) +{ + out[0] = in & 0xff; + out[1] = (in >> 8) & 0xff; + out[2] = (in >> 16) & 0xff; + out[3] = (in >> 24) & 0xff; +} + +static void store64_le(u8 out[8], u64 in) +{ + store32_le(out , (u32)in ); + store32_le(out + 4, in >> 32); +} + +static void load32_le_buf (u32 *dst, const u8 *src, size_t size) { + FOR(i, 0, size) { dst[i] = load32_le(src + i*4); } +} +static void load64_le_buf (u64 *dst, const u8 *src, size_t size) { + FOR(i, 0, size) { dst[i] = load64_le(src + i*8); } +} +static void store32_le_buf(u8 *dst, const u32 *src, size_t size) { + FOR(i, 0, size) { store32_le(dst + i*4, src[i]); } +} +static void store64_le_buf(u8 *dst, const u64 *src, size_t size) { + FOR(i, 0, size) { store64_le(dst + i*8, src[i]); } +} + +static u64 rotr64(u64 x, u64 n) { return (x >> n) ^ (x << (64 - n)); } +static u32 rotl32(u32 x, u32 n) { return (x << n) ^ (x >> (32 - n)); } + +static int neq0(u64 diff) +{ + // constant time comparison to zero + // return diff != 0 ? -1 : 0 + u64 half = (diff >> 32) | ((u32)diff); + return (1 & ((half - 1) >> 32)) - 1; +} + +static u64 x16(const u8 a[16], const u8 b[16]) +{ + return (load64_le(a + 0) ^ load64_le(b + 0)) + | (load64_le(a + 8) ^ load64_le(b + 8)); +} +static u64 x32(const u8 a[32],const u8 b[32]){return x16(a,b)| x16(a+16, b+16);} +static u64 x64(const u8 a[64],const u8 b[64]){return x32(a,b)| x32(a+32, b+32);} +int crypto_verify16(const u8 a[16], const u8 b[16]){ return neq0(x16(a, b)); } +int crypto_verify32(const u8 a[32], const u8 b[32]){ return neq0(x32(a, b)); } +int crypto_verify64(const u8 a[64], const u8 b[64]){ return neq0(x64(a, b)); } + +void crypto_wipe(void *secret, size_t size) +{ + volatile u8 *v_secret = (u8*)secret; + ZERO(v_secret, size); +} + +///////////////// +/// Chacha 20 /// +///////////////// +#define QUARTERROUND(a, b, c, d) \ + a += b; d = rotl32(d ^ a, 16); \ + c += d; b = rotl32(b ^ c, 12); \ + a += b; d = rotl32(d ^ a, 8); \ + c += d; b = rotl32(b ^ c, 7) + +static void chacha20_rounds(u32 out[16], const u32 in[16]) +{ + // The temporary variables make Chacha20 10% faster. + u32 t0 = in[ 0]; u32 t1 = in[ 1]; u32 t2 = in[ 2]; u32 t3 = in[ 3]; + u32 t4 = in[ 4]; u32 t5 = in[ 5]; u32 t6 = in[ 6]; u32 t7 = in[ 7]; + u32 t8 = in[ 8]; u32 t9 = in[ 9]; u32 t10 = in[10]; u32 t11 = in[11]; + u32 t12 = in[12]; u32 t13 = in[13]; u32 t14 = in[14]; u32 t15 = in[15]; + + FOR (i, 0, 10) { // 20 rounds, 2 rounds per loop. + QUARTERROUND(t0, t4, t8 , t12); // column 0 + QUARTERROUND(t1, t5, t9 , t13); // column 1 + QUARTERROUND(t2, t6, t10, t14); // column 2 + QUARTERROUND(t3, t7, t11, t15); // column 3 + QUARTERROUND(t0, t5, t10, t15); // diagonal 0 + QUARTERROUND(t1, t6, t11, t12); // diagonal 1 + QUARTERROUND(t2, t7, t8 , t13); // diagonal 2 + QUARTERROUND(t3, t4, t9 , t14); // diagonal 3 + } + out[ 0] = t0; out[ 1] = t1; out[ 2] = t2; out[ 3] = t3; + out[ 4] = t4; out[ 5] = t5; out[ 6] = t6; out[ 7] = t7; + out[ 8] = t8; out[ 9] = t9; out[10] = t10; out[11] = t11; + out[12] = t12; out[13] = t13; out[14] = t14; out[15] = t15; +} + +static const u8 *chacha20_constant = (const u8*)"expand 32-byte k"; // 16 bytes + +void crypto_chacha20_h(u8 out[32], const u8 key[32], const u8 in [16]) +{ + u32 block[16]; + load32_le_buf(block , chacha20_constant, 4); + load32_le_buf(block + 4, key , 8); + load32_le_buf(block + 12, in , 4); + + chacha20_rounds(block, block); + + // prevent reversal of the rounds by revealing only half of the buffer. + store32_le_buf(out , block , 4); // constant + store32_le_buf(out+16, block+12, 4); // counter and nonce + WIPE_BUFFER(block); +} + +u64 crypto_chacha20_djb(u8 *cipher_text, const u8 *plain_text, + size_t text_size, const u8 key[32], const u8 nonce[8], + u64 ctr) +{ + u32 input[16]; + load32_le_buf(input , chacha20_constant, 4); + load32_le_buf(input + 4, key , 8); + load32_le_buf(input + 14, nonce , 2); + input[12] = (u32) ctr; + input[13] = (u32)(ctr >> 32); + + // Whole blocks + u32 pool[16]; + size_t nb_blocks = text_size >> 6; + FOR (i, 0, nb_blocks) { + chacha20_rounds(pool, input); + if (plain_text != 0) { + FOR (j, 0, 16) { + u32 p = pool[j] + input[j]; + store32_le(cipher_text, p ^ load32_le(plain_text)); + cipher_text += 4; + plain_text += 4; + } + } else { + FOR (j, 0, 16) { + u32 p = pool[j] + input[j]; + store32_le(cipher_text, p); + cipher_text += 4; + } + } + input[12]++; + if (input[12] == 0) { + input[13]++; + } + } + text_size &= 63; + + // Last (incomplete) block + if (text_size > 0) { + if (plain_text == 0) { + plain_text = zero; + } + chacha20_rounds(pool, input); + u8 tmp[64]; + FOR (i, 0, 16) { + store32_le(tmp + i*4, pool[i] + input[i]); + } + FOR (i, 0, text_size) { + cipher_text[i] = tmp[i] ^ plain_text[i]; + } + WIPE_BUFFER(tmp); + } + ctr = input[12] + ((u64)input[13] << 32) + (text_size > 0); + + WIPE_BUFFER(pool); + WIPE_BUFFER(input); + return ctr; +} + +u32 crypto_chacha20_ietf(u8 *cipher_text, const u8 *plain_text, + size_t text_size, + const u8 key[32], const u8 nonce[12], u32 ctr) +{ + u64 big_ctr = ctr + ((u64)load32_le(nonce) << 32); + return (u32)crypto_chacha20_djb(cipher_text, plain_text, text_size, + key, nonce + 4, big_ctr); +} + +u64 crypto_chacha20_x(u8 *cipher_text, const u8 *plain_text, + size_t text_size, + const u8 key[32], const u8 nonce[24], u64 ctr) +{ + u8 sub_key[32]; + crypto_chacha20_h(sub_key, key, nonce); + ctr = crypto_chacha20_djb(cipher_text, plain_text, text_size, + sub_key, nonce + 16, ctr); + WIPE_BUFFER(sub_key); + return ctr; +} + +///////////////// +/// Poly 1305 /// +///////////////// + +// h = (h + c) * r +// preconditions: +// ctx->h <= 4_ffffffff_ffffffff_ffffffff_ffffffff +// ctx->r <= 0ffffffc_0ffffffc_0ffffffc_0fffffff +// end <= 1 +// Postcondition: +// ctx->h <= 4_ffffffff_ffffffff_ffffffff_ffffffff +static void poly_blocks(crypto_poly1305_ctx *ctx, const u8 *in, + size_t nb_blocks, unsigned end) +{ + // Local all the things! + const u32 r0 = ctx->r[0]; + const u32 r1 = ctx->r[1]; + const u32 r2 = ctx->r[2]; + const u32 r3 = ctx->r[3]; + const u32 rr0 = (r0 >> 2) * 5; // lose 2 bits... + const u32 rr1 = (r1 >> 2) + r1; // rr1 == (r1 >> 2) * 5 + const u32 rr2 = (r2 >> 2) + r2; // rr1 == (r2 >> 2) * 5 + const u32 rr3 = (r3 >> 2) + r3; // rr1 == (r3 >> 2) * 5 + const u32 rr4 = r0 & 3; // ...recover 2 bits + u32 h0 = ctx->h[0]; + u32 h1 = ctx->h[1]; + u32 h2 = ctx->h[2]; + u32 h3 = ctx->h[3]; + u32 h4 = ctx->h[4]; + + FOR (i, 0, nb_blocks) { + // h + c, without carry propagation + const u64 s0 = (u64)h0 + load32_le(in); in += 4; + const u64 s1 = (u64)h1 + load32_le(in); in += 4; + const u64 s2 = (u64)h2 + load32_le(in); in += 4; + const u64 s3 = (u64)h3 + load32_le(in); in += 4; + const u32 s4 = h4 + end; + + // (h + c) * r, without carry propagation + const u64 x0 = s0*r0+ s1*rr3+ s2*rr2+ s3*rr1+ s4*rr0; + const u64 x1 = s0*r1+ s1*r0 + s2*rr3+ s3*rr2+ s4*rr1; + const u64 x2 = s0*r2+ s1*r1 + s2*r0 + s3*rr3+ s4*rr2; + const u64 x3 = s0*r3+ s1*r2 + s2*r1 + s3*r0 + s4*rr3; + const u32 x4 = s4*rr4; + + // partial reduction modulo 2^130 - 5 + const u32 u5 = x4 + (x3 >> 32); // u5 <= 7ffffff5 + const u64 u0 = (u5 >> 2) * 5 + (x0 & 0xffffffff); + const u64 u1 = (u0 >> 32) + (x1 & 0xffffffff) + (x0 >> 32); + const u64 u2 = (u1 >> 32) + (x2 & 0xffffffff) + (x1 >> 32); + const u64 u3 = (u2 >> 32) + (x3 & 0xffffffff) + (x2 >> 32); + const u32 u4 = (u3 >> 32) + (u5 & 3); // u4 <= 4 + + // Update the hash + h0 = u0 & 0xffffffff; + h1 = u1 & 0xffffffff; + h2 = u2 & 0xffffffff; + h3 = u3 & 0xffffffff; + h4 = u4; + } + ctx->h[0] = h0; + ctx->h[1] = h1; + ctx->h[2] = h2; + ctx->h[3] = h3; + ctx->h[4] = h4; +} + +void crypto_poly1305_init(crypto_poly1305_ctx *ctx, const u8 key[32]) +{ + ZERO(ctx->h, 5); // Initial hash is zero + ctx->c_idx = 0; + // load r and pad (r has some of its bits cleared) + load32_le_buf(ctx->r , key , 4); + load32_le_buf(ctx->pad, key+16, 4); + FOR (i, 0, 1) { ctx->r[i] &= 0x0fffffff; } + FOR (i, 1, 4) { ctx->r[i] &= 0x0ffffffc; } +} + +void crypto_poly1305_update(crypto_poly1305_ctx *ctx, + const u8 *message, size_t message_size) +{ + // Avoid undefined NULL pointer increments with empty messages + if (message_size == 0) { + return; + } + + // Align ourselves with block boundaries + size_t aligned = MIN(gap(ctx->c_idx, 16), message_size); + FOR (i, 0, aligned) { + ctx->c[ctx->c_idx] = *message; + ctx->c_idx++; + message++; + message_size--; + } + + // If block is complete, process it + if (ctx->c_idx == 16) { + poly_blocks(ctx, ctx->c, 1, 1); + ctx->c_idx = 0; + } + + // Process the message block by block + size_t nb_blocks = message_size >> 4; + poly_blocks(ctx, message, nb_blocks, 1); + message += nb_blocks << 4; + message_size &= 15; + + // remaining bytes (we never complete a block here) + FOR (i, 0, message_size) { + ctx->c[ctx->c_idx] = message[i]; + ctx->c_idx++; + } +} + +void crypto_poly1305_final(crypto_poly1305_ctx *ctx, u8 mac[16]) +{ + // Process the last block (if any) + // We move the final 1 according to remaining input length + // (this will add less than 2^130 to the last input block) + if (ctx->c_idx != 0) { + ZERO(ctx->c + ctx->c_idx, 16 - ctx->c_idx); + ctx->c[ctx->c_idx] = 1; + poly_blocks(ctx, ctx->c, 1, 0); + } + + // check if we should subtract 2^130-5 by performing the + // corresponding carry propagation. + u64 c = 5; + FOR (i, 0, 4) { + c += ctx->h[i]; + c >>= 32; + } + c += ctx->h[4]; + c = (c >> 2) * 5; // shift the carry back to the beginning + // c now indicates how many times we should subtract 2^130-5 (0 or 1) + FOR (i, 0, 4) { + c += (u64)ctx->h[i] + ctx->pad[i]; + store32_le(mac + i*4, (u32)c); + c = c >> 32; + } + WIPE_CTX(ctx); +} + +void crypto_poly1305(u8 mac[16], const u8 *message, + size_t message_size, const u8 key[32]) +{ + crypto_poly1305_ctx ctx; + crypto_poly1305_init (&ctx, key); + crypto_poly1305_update(&ctx, message, message_size); + crypto_poly1305_final (&ctx, mac); +} + +//////////////// +/// BLAKE2 b /// +//////////////// +static const u64 iv[8] = { + 0x6a09e667f3bcc908, 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179, +}; + +static void blake2b_compress(crypto_blake2b_ctx *ctx, int is_last_block) +{ + static const u8 sigma[12][16] = { + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, + { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 }, + { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 }, + { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 }, + { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 }, + { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 }, + { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 }, + { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 }, + { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 }, + { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, + { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, + }; + + // increment input offset + u64 *x = ctx->input_offset; + size_t y = ctx->input_idx; + x[0] += y; + if (x[0] < y) { + x[1]++; + } + + // init work vector + u64 v0 = ctx->hash[0]; u64 v8 = iv[0]; + u64 v1 = ctx->hash[1]; u64 v9 = iv[1]; + u64 v2 = ctx->hash[2]; u64 v10 = iv[2]; + u64 v3 = ctx->hash[3]; u64 v11 = iv[3]; + u64 v4 = ctx->hash[4]; u64 v12 = iv[4] ^ ctx->input_offset[0]; + u64 v5 = ctx->hash[5]; u64 v13 = iv[5] ^ ctx->input_offset[1]; + u64 v6 = ctx->hash[6]; u64 v14 = iv[6] ^ (u64)~(is_last_block - 1); + u64 v7 = ctx->hash[7]; u64 v15 = iv[7]; + + // mangle work vector + u64 *input = ctx->input; +#define BLAKE2_G(a, b, c, d, x, y) \ + a += b + x; d = rotr64(d ^ a, 32); \ + c += d; b = rotr64(b ^ c, 24); \ + a += b + y; d = rotr64(d ^ a, 16); \ + c += d; b = rotr64(b ^ c, 63) +#define BLAKE2_ROUND(i) \ + BLAKE2_G(v0, v4, v8 , v12, input[sigma[i][ 0]], input[sigma[i][ 1]]); \ + BLAKE2_G(v1, v5, v9 , v13, input[sigma[i][ 2]], input[sigma[i][ 3]]); \ + BLAKE2_G(v2, v6, v10, v14, input[sigma[i][ 4]], input[sigma[i][ 5]]); \ + BLAKE2_G(v3, v7, v11, v15, input[sigma[i][ 6]], input[sigma[i][ 7]]); \ + BLAKE2_G(v0, v5, v10, v15, input[sigma[i][ 8]], input[sigma[i][ 9]]); \ + BLAKE2_G(v1, v6, v11, v12, input[sigma[i][10]], input[sigma[i][11]]); \ + BLAKE2_G(v2, v7, v8 , v13, input[sigma[i][12]], input[sigma[i][13]]); \ + BLAKE2_G(v3, v4, v9 , v14, input[sigma[i][14]], input[sigma[i][15]]) + +#ifdef BLAKE2_NO_UNROLLING + FOR (i, 0, 12) { + BLAKE2_ROUND(i); + } +#else + BLAKE2_ROUND(0); BLAKE2_ROUND(1); BLAKE2_ROUND(2); BLAKE2_ROUND(3); + BLAKE2_ROUND(4); BLAKE2_ROUND(5); BLAKE2_ROUND(6); BLAKE2_ROUND(7); + BLAKE2_ROUND(8); BLAKE2_ROUND(9); BLAKE2_ROUND(10); BLAKE2_ROUND(11); +#endif + + // update hash + ctx->hash[0] ^= v0 ^ v8; ctx->hash[1] ^= v1 ^ v9; + ctx->hash[2] ^= v2 ^ v10; ctx->hash[3] ^= v3 ^ v11; + ctx->hash[4] ^= v4 ^ v12; ctx->hash[5] ^= v5 ^ v13; + ctx->hash[6] ^= v6 ^ v14; ctx->hash[7] ^= v7 ^ v15; +} + +void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size, + const u8 *key, size_t key_size) +{ + // initial hash + COPY(ctx->hash, iv, 8); + ctx->hash[0] ^= 0x01010000 ^ (key_size << 8) ^ hash_size; + + ctx->input_offset[0] = 0; // beginning of the input, no offset + ctx->input_offset[1] = 0; // beginning of the input, no offset + ctx->hash_size = hash_size; + ctx->input_idx = 0; + ZERO(ctx->input, 16); + + // if there is a key, the first block is that key (padded with zeroes) + if (key_size > 0) { + u8 key_block[128] = {0}; + COPY(key_block, key, key_size); + // same as calling crypto_blake2b_update(ctx, key_block , 128) + load64_le_buf(ctx->input, key_block, 16); + ctx->input_idx = 128; + } +} + +void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size) +{ + crypto_blake2b_keyed_init(ctx, hash_size, 0, 0); +} + +void crypto_blake2b_update(crypto_blake2b_ctx *ctx, + const u8 *message, size_t message_size) +{ + // Avoid undefined NULL pointer increments with empty messages + if (message_size == 0) { + return; + } + + // Align with word boundaries + if ((ctx->input_idx & 7) != 0) { + size_t nb_bytes = MIN(gap(ctx->input_idx, 8), message_size); + size_t word = ctx->input_idx >> 3; + size_t byte = ctx->input_idx & 7; + FOR (i, 0, nb_bytes) { + ctx->input[word] |= (u64)message[i] << ((byte + i) << 3); + } + ctx->input_idx += nb_bytes; + message += nb_bytes; + message_size -= nb_bytes; + } + + // Align with block boundaries (faster than byte by byte) + if ((ctx->input_idx & 127) != 0) { + size_t nb_words = MIN(gap(ctx->input_idx, 128), message_size) >> 3; + load64_le_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words); + ctx->input_idx += nb_words << 3; + message += nb_words << 3; + message_size -= nb_words << 3; + } + + // Process block by block + size_t nb_blocks = message_size >> 7; + FOR (i, 0, nb_blocks) { + if (ctx->input_idx == 128) { + blake2b_compress(ctx, 0); + } + load64_le_buf(ctx->input, message, 16); + message += 128; + ctx->input_idx = 128; + } + message_size &= 127; + + if (message_size != 0) { + // Compress block & flush input buffer as needed + if (ctx->input_idx == 128) { + blake2b_compress(ctx, 0); + ctx->input_idx = 0; + } + if (ctx->input_idx == 0) { + ZERO(ctx->input, 16); + } + // Fill remaining words (faster than byte by byte) + size_t nb_words = message_size >> 3; + load64_le_buf(ctx->input, message, nb_words); + ctx->input_idx += nb_words << 3; + message += nb_words << 3; + message_size -= nb_words << 3; + + // Fill remaining bytes + FOR (i, 0, message_size) { + size_t word = ctx->input_idx >> 3; + size_t byte = ctx->input_idx & 7; + ctx->input[word] |= (u64)message[i] << (byte << 3); + ctx->input_idx++; + } + } +} + +void crypto_blake2b_final(crypto_blake2b_ctx *ctx, u8 *hash) +{ + blake2b_compress(ctx, 1); // compress the last block + size_t hash_size = MIN(ctx->hash_size, 64); + size_t nb_words = hash_size >> 3; + store64_le_buf(hash, ctx->hash, nb_words); + FOR (i, nb_words << 3, hash_size) { + hash[i] = (ctx->hash[i >> 3] >> (8 * (i & 7))) & 0xff; + } + WIPE_CTX(ctx); +} + +void crypto_blake2b_keyed(u8 *hash, size_t hash_size, + const u8 *key, size_t key_size, + const u8 *message, size_t message_size) +{ + crypto_blake2b_ctx ctx; + crypto_blake2b_keyed_init(&ctx, hash_size, key, key_size); + crypto_blake2b_update (&ctx, message, message_size); + crypto_blake2b_final (&ctx, hash); +} + +void crypto_blake2b(u8 *hash, size_t hash_size, const u8 *msg, size_t msg_size) +{ + crypto_blake2b_keyed(hash, hash_size, 0, 0, msg, msg_size); +} + +////////////// +/// Argon2 /// +////////////// +// references to R, Z, Q etc. come from the spec + +// Argon2 operates on 1024 byte blocks. +typedef struct { u64 a[128]; } blk; + +// updates a BLAKE2 hash with a 32 bit word, little endian. +static void blake_update_32(crypto_blake2b_ctx *ctx, u32 input) +{ + u8 buf[4]; + store32_le(buf, input); + crypto_blake2b_update(ctx, buf, 4); + WIPE_BUFFER(buf); +} + +static void blake_update_32_buf(crypto_blake2b_ctx *ctx, + const u8 *buf, u32 size) +{ + blake_update_32(ctx, size); + crypto_blake2b_update(ctx, buf, size); +} + + +static void copy_block(blk *o,const blk*in){FOR(i, 0, 128) o->a[i] = in->a[i];} +static void xor_block(blk *o,const blk*in){FOR(i, 0, 128) o->a[i] ^= in->a[i];} + +// Hash with a virtually unlimited digest size. +// Doesn't extract more entropy than the base hash function. +// Mainly used for filling a whole kilobyte block with pseudo-random bytes. +// (One could use a stream cipher with a seed hash as the key, but +// this would introduce another dependency —and point of failure.) +static void extended_hash(u8 *digest, u32 digest_size, + const u8 *input , u32 input_size) +{ + crypto_blake2b_ctx ctx; + crypto_blake2b_init (&ctx, MIN(digest_size, 64)); + blake_update_32 (&ctx, digest_size); + crypto_blake2b_update(&ctx, input, input_size); + crypto_blake2b_final (&ctx, digest); + + if (digest_size > 64) { + // the conversion to u64 avoids integer overflow on + // ludicrously big hash sizes. + u32 r = (u32)(((u64)digest_size + 31) >> 5) - 2; + u32 i = 1; + u32 in = 0; + u32 out = 32; + while (i < r) { + // Input and output overlap. This is intentional + crypto_blake2b(digest + out, 64, digest + in, 64); + i += 1; + in += 32; + out += 32; + } + crypto_blake2b(digest + out, digest_size - (32 * r), digest + in , 64); + } +} + +#define LSB(x) ((u64)(u32)x) +#define G(a, b, c, d) \ + a += b + ((LSB(a) * LSB(b)) << 1); d ^= a; d = rotr64(d, 32); \ + c += d + ((LSB(c) * LSB(d)) << 1); b ^= c; b = rotr64(b, 24); \ + a += b + ((LSB(a) * LSB(b)) << 1); d ^= a; d = rotr64(d, 16); \ + c += d + ((LSB(c) * LSB(d)) << 1); b ^= c; b = rotr64(b, 63) +#define ROUND(v0, v1, v2, v3, v4, v5, v6, v7, \ + v8, v9, v10, v11, v12, v13, v14, v15) \ + G(v0, v4, v8, v12); G(v1, v5, v9, v13); \ + G(v2, v6, v10, v14); G(v3, v7, v11, v15); \ + G(v0, v5, v10, v15); G(v1, v6, v11, v12); \ + G(v2, v7, v8, v13); G(v3, v4, v9, v14) + +// Core of the compression function G. Computes Z from R in place. +static void g_rounds(blk *b) +{ + // column rounds (work_block = Q) + for (int i = 0; i < 128; i += 16) { + ROUND(b->a[i ], b->a[i+ 1], b->a[i+ 2], b->a[i+ 3], + b->a[i+ 4], b->a[i+ 5], b->a[i+ 6], b->a[i+ 7], + b->a[i+ 8], b->a[i+ 9], b->a[i+10], b->a[i+11], + b->a[i+12], b->a[i+13], b->a[i+14], b->a[i+15]); + } + // row rounds (b = Z) + for (int i = 0; i < 16; i += 2) { + ROUND(b->a[i ], b->a[i+ 1], b->a[i+ 16], b->a[i+ 17], + b->a[i+32], b->a[i+33], b->a[i+ 48], b->a[i+ 49], + b->a[i+64], b->a[i+65], b->a[i+ 80], b->a[i+ 81], + b->a[i+96], b->a[i+97], b->a[i+112], b->a[i+113]); + } +} + +const crypto_argon2_extras crypto_argon2_no_extras = { 0, 0, 0, 0 }; + +void crypto_argon2(u8 *hash, u32 hash_size, void *work_area, + crypto_argon2_config config, + crypto_argon2_inputs inputs, + crypto_argon2_extras extras) +{ + const u32 segment_size = config.nb_blocks / config.nb_lanes / 4; + const u32 lane_size = segment_size * 4; + const u32 nb_blocks = lane_size * config.nb_lanes; // rounding down + + // work area seen as blocks (must be suitably aligned) + blk *blocks = (blk*)work_area; + { + u8 initial_hash[72]; // 64 bytes plus 2 words for future hashes + crypto_blake2b_ctx ctx; + crypto_blake2b_init (&ctx, 64); + blake_update_32 (&ctx, config.nb_lanes ); // p: number of "threads" + blake_update_32 (&ctx, hash_size); + blake_update_32 (&ctx, config.nb_blocks); + blake_update_32 (&ctx, config.nb_passes); + blake_update_32 (&ctx, 0x13); // v: version number + blake_update_32 (&ctx, config.algorithm); // y: Argon2i, Argon2d... + blake_update_32_buf (&ctx, inputs.pass, inputs.pass_size); + blake_update_32_buf (&ctx, inputs.salt, inputs.salt_size); + blake_update_32_buf (&ctx, extras.key, extras.key_size); + blake_update_32_buf (&ctx, extras.ad, extras.ad_size); + crypto_blake2b_final(&ctx, initial_hash); // fill 64 first bytes only + + // fill first 2 blocks of each lane + u8 hash_area[1024]; + FOR_T(u32, l, 0, config.nb_lanes) { + FOR_T(u32, i, 0, 2) { + store32_le(initial_hash + 64, i); // first additional word + store32_le(initial_hash + 68, l); // second additional word + extended_hash(hash_area, 1024, initial_hash, 72); + load64_le_buf(blocks[l * lane_size + i].a, hash_area, 128); + } + } + + WIPE_BUFFER(initial_hash); + WIPE_BUFFER(hash_area); + } + + // Argon2i and Argon2id start with constant time indexing + int constant_time = config.algorithm != CRYPTO_ARGON2_D; + + // Fill (and re-fill) the rest of the blocks + // + // Note: even though each segment within the same slice can be + // computed in parallel, (one thread per lane), we are computing + // them sequentially, because Monocypher doesn't support threads. + // + // Yet optimal performance (and therefore security) requires one + // thread per lane. The only reason Monocypher supports multiple + // lanes is compatibility. + blk tmp; + FOR_T(u32, pass, 0, config.nb_passes) { + FOR_T(u32, slice, 0, 4) { + // On the first slice of the first pass, + // blocks 0 and 1 are already filled, hence pass_offset. + u32 pass_offset = pass == 0 && slice == 0 ? 2 : 0; + u32 slice_offset = slice * segment_size; + + // Argon2id switches back to non-constant time indexing + // after the first two slices of the first pass + if (slice == 2 && config.algorithm == CRYPTO_ARGON2_ID) { + constant_time = 0; + } + + // Each iteration of the following loop may be performed in + // a separate thread. All segments must be fully completed + // before we start filling the next slice. + FOR_T(u32, segment, 0, config.nb_lanes) { + blk index_block; + u32 index_ctr = 1; + FOR_T (u32, block, pass_offset, segment_size) { + // Current and previous blocks + u32 lane_offset = segment * lane_size; + blk *segment_start = blocks + lane_offset + slice_offset; + blk *current = segment_start + block; + blk *previous = + block == 0 && slice_offset == 0 + ? segment_start + lane_size - 1 + : segment_start + block - 1; + + u64 index_seed; + if (constant_time) { + if (block == pass_offset || (block % 128) == 0) { + // Fill or refresh deterministic indices block + + // seed the beginning of the block... + ZERO(index_block.a, 128); + index_block.a[0] = pass; + index_block.a[1] = segment; + index_block.a[2] = slice; + index_block.a[3] = nb_blocks; + index_block.a[4] = config.nb_passes; + index_block.a[5] = config.algorithm; + index_block.a[6] = index_ctr; + index_ctr++; + + // ... then shuffle it + copy_block(&tmp, &index_block); + g_rounds (&index_block); + xor_block (&index_block, &tmp); + copy_block(&tmp, &index_block); + g_rounds (&index_block); + xor_block (&index_block, &tmp); + } + index_seed = index_block.a[block % 128]; + } else { + index_seed = previous->a[0]; + } + + // Establish the reference set. *Approximately* comprises: + // - The last 3 slices (if they exist yet) + // - The already constructed blocks in the current segment + u32 next_slice = ((slice + 1) % 4) * segment_size; + u32 window_start = pass == 0 ? 0 : next_slice; + u32 nb_segments = pass == 0 ? slice : 3; + u64 lane = + pass == 0 && slice == 0 + ? segment + : (index_seed >> 32) % config.nb_lanes; + u32 window_size = + nb_segments * segment_size + + (lane == segment ? block-1 : + block == 0 ? (u32)-1 : 0); + + // Find reference block + u64 j1 = index_seed & 0xffffffff; // block selector + u64 x = (j1 * j1) >> 32; + u64 y = (window_size * x) >> 32; + u64 z = (window_size - 1) - y; + u64 ref = (window_start + z) % lane_size; + u32 index = lane * lane_size + (u32)ref; + blk *reference = blocks + index; + + // Shuffle the previous & reference block + // into the current block + copy_block(&tmp, previous); + xor_block (&tmp, reference); + if (pass == 0) { copy_block(current, &tmp); } + else { xor_block (current, &tmp); } + g_rounds (&tmp); + xor_block (current, &tmp); + } + } + } + } + + // Wipe temporary block + volatile u64* p = tmp.a; + ZERO(p, 128); + + // XOR last blocks of each lane + blk *last_block = blocks + lane_size - 1; + FOR_T (u32, lane, 1, config.nb_lanes) { + blk *next_block = last_block + lane_size; + xor_block(next_block, last_block); + last_block = next_block; + } + + // Serialize last block + u8 final_block[1024]; + store64_le_buf(final_block, last_block->a, 128); + + // Wipe work area + p = (u64*)work_area; + ZERO(p, 128 * nb_blocks); + + // Hash the very last block with H' into the output hash + extended_hash(hash, hash_size, final_block, 1024); + WIPE_BUFFER(final_block); +} + +//////////////////////////////////// +/// Arithmetic modulo 2^255 - 19 /// +//////////////////////////////////// +// Originally taken from SUPERCOP's ref10 implementation. +// A bit bigger than TweetNaCl, over 4 times faster. + +// field element +typedef i32 fe[10]; + +// field constants +// +// fe_one : 1 +// sqrtm1 : sqrt(-1) +// d : -121665 / 121666 +// D2 : 2 * -121665 / 121666 +// lop_x, lop_y: low order point in Edwards coordinates +// ufactor : -sqrt(-1) * 2 +// A2 : 486662^2 (A squared) +static const fe fe_one = {1}; +static const fe sqrtm1 = { + -32595792, -7943725, 9377950, 3500415, 12389472, + -272473, -25146209, -2005654, 326686, 11406482, +}; +static const fe d = { + -10913610, 13857413, -15372611, 6949391, 114729, + -8787816, -6275908, -3247719, -18696448, -12055116, +}; +static const fe D2 = { + -21827239, -5839606, -30745221, 13898782, 229458, + 15978800, -12551817, -6495438, 29715968, 9444199, +}; +static const fe lop_x = { + 21352778, 5345713, 4660180, -8347857, 24143090, + 14568123, 30185756, -12247770, -33528939, 8345319, +}; +static const fe lop_y = { + -6952922, -1265500, 6862341, -7057498, -4037696, + -5447722, 31680899, -15325402, -19365852, 1569102, +}; +static const fe ufactor = { + -1917299, 15887451, -18755900, -7000830, -24778944, + 544946, -16816446, 4011309, -653372, 10741468, +}; +static const fe A2 = { + 12721188, 3529, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static void fe_0(fe h) { ZERO(h , 10); } +static void fe_1(fe h) { h[0] = 1; ZERO(h+1, 9); } + +static void fe_copy(fe h,const fe f ){FOR(i,0,10) h[i] = f[i]; } +static void fe_neg (fe h,const fe f ){FOR(i,0,10) h[i] = -f[i]; } +static void fe_add (fe h,const fe f,const fe g){FOR(i,0,10) h[i] = f[i] + g[i];} +static void fe_sub (fe h,const fe f,const fe g){FOR(i,0,10) h[i] = f[i] - g[i];} + +static void fe_cswap(fe f, fe g, int b) +{ + i32 mask = -b; // -1 = 0xffffffff + FOR (i, 0, 10) { + i32 x = (f[i] ^ g[i]) & mask; + f[i] = f[i] ^ x; + g[i] = g[i] ^ x; + } +} + +static void fe_ccopy(fe f, const fe g, int b) +{ + i32 mask = -b; // -1 = 0xffffffff + FOR (i, 0, 10) { + i32 x = (f[i] ^ g[i]) & mask; + f[i] = f[i] ^ x; + } +} + + +// Signed carry propagation +// ------------------------ +// +// Let t be a number. It can be uniquely decomposed thus: +// +// t = h*2^26 + l +// such that -2^25 <= l < 2^25 +// +// Let c = (t + 2^25) / 2^26 (rounded down) +// c = (h*2^26 + l + 2^25) / 2^26 (rounded down) +// c = h + (l + 2^25) / 2^26 (rounded down) +// c = h (exactly) +// Because 0 <= l + 2^25 < 2^26 +// +// Let u = t - c*2^26 +// u = h*2^26 + l - h*2^26 +// u = l +// Therefore, -2^25 <= u < 2^25 +// +// Additionally, if |t| < x, then |h| < x/2^26 (rounded down) +// +// Notations: +// - In C, 1<<25 means 2^25. +// - In C, x>>25 means floor(x / (2^25)). +// - All of the above applies with 25 & 24 as well as 26 & 25. +// +// +// Note on negative right shifts +// ----------------------------- +// +// In C, x >> n, where x is a negative integer, is implementation +// defined. In practice, all platforms do arithmetic shift, which is +// equivalent to division by 2^26, rounded down. Some compilers, like +// GCC, even guarantee it. +// +// If we ever stumble upon a platform that does not propagate the sign +// bit (we won't), visible failures will show at the slightest test, and +// the signed shifts can be replaced by the following: +// +// typedef struct { i64 x:39; } s25; +// typedef struct { i64 x:38; } s26; +// i64 shift25(i64 x) { s25 s; s.x = ((u64)x)>>25; return s.x; } +// i64 shift26(i64 x) { s26 s; s.x = ((u64)x)>>26; return s.x; } +// +// Current compilers cannot optimise this, causing a 30% drop in +// performance. Fairly expensive for something that never happens. +// +// +// Precondition +// ------------ +// +// |t0| < 2^63 +// |t1|..|t9| < 2^62 +// +// Algorithm +// --------- +// c = t0 + 2^25 / 2^26 -- |c| <= 2^36 +// t0 -= c * 2^26 -- |t0| <= 2^25 +// t1 += c -- |t1| <= 2^63 +// +// c = t4 + 2^25 / 2^26 -- |c| <= 2^36 +// t4 -= c * 2^26 -- |t4| <= 2^25 +// t5 += c -- |t5| <= 2^63 +// +// c = t1 + 2^24 / 2^25 -- |c| <= 2^38 +// t1 -= c * 2^25 -- |t1| <= 2^24 +// t2 += c -- |t2| <= 2^63 +// +// c = t5 + 2^24 / 2^25 -- |c| <= 2^38 +// t5 -= c * 2^25 -- |t5| <= 2^24 +// t6 += c -- |t6| <= 2^63 +// +// c = t2 + 2^25 / 2^26 -- |c| <= 2^37 +// t2 -= c * 2^26 -- |t2| <= 2^25 < 1.1 * 2^25 (final t2) +// t3 += c -- |t3| <= 2^63 +// +// c = t6 + 2^25 / 2^26 -- |c| <= 2^37 +// t6 -= c * 2^26 -- |t6| <= 2^25 < 1.1 * 2^25 (final t6) +// t7 += c -- |t7| <= 2^63 +// +// c = t3 + 2^24 / 2^25 -- |c| <= 2^38 +// t3 -= c * 2^25 -- |t3| <= 2^24 < 1.1 * 2^24 (final t3) +// t4 += c -- |t4| <= 2^25 + 2^38 < 2^39 +// +// c = t7 + 2^24 / 2^25 -- |c| <= 2^38 +// t7 -= c * 2^25 -- |t7| <= 2^24 < 1.1 * 2^24 (final t7) +// t8 += c -- |t8| <= 2^63 +// +// c = t4 + 2^25 / 2^26 -- |c| <= 2^13 +// t4 -= c * 2^26 -- |t4| <= 2^25 < 1.1 * 2^25 (final t4) +// t5 += c -- |t5| <= 2^24 + 2^13 < 1.1 * 2^24 (final t5) +// +// c = t8 + 2^25 / 2^26 -- |c| <= 2^37 +// t8 -= c * 2^26 -- |t8| <= 2^25 < 1.1 * 2^25 (final t8) +// t9 += c -- |t9| <= 2^63 +// +// c = t9 + 2^24 / 2^25 -- |c| <= 2^38 +// t9 -= c * 2^25 -- |t9| <= 2^24 < 1.1 * 2^24 (final t9) +// t0 += c * 19 -- |t0| <= 2^25 + 2^38*19 < 2^44 +// +// c = t0 + 2^25 / 2^26 -- |c| <= 2^18 +// t0 -= c * 2^26 -- |t0| <= 2^25 < 1.1 * 2^25 (final t0) +// t1 += c -- |t1| <= 2^24 + 2^18 < 1.1 * 2^24 (final t1) +// +// Postcondition +// ------------- +// |t0|, |t2|, |t4|, |t6|, |t8| < 1.1 * 2^25 +// |t1|, |t3|, |t5|, |t7|, |t9| < 1.1 * 2^24 +#define FE_CARRY \ + i64 c; \ + c = (t0 + ((i64)1<<25)) >> 26; t0 -= c * ((i64)1 << 26); t1 += c; \ + c = (t4 + ((i64)1<<25)) >> 26; t4 -= c * ((i64)1 << 26); t5 += c; \ + c = (t1 + ((i64)1<<24)) >> 25; t1 -= c * ((i64)1 << 25); t2 += c; \ + c = (t5 + ((i64)1<<24)) >> 25; t5 -= c * ((i64)1 << 25); t6 += c; \ + c = (t2 + ((i64)1<<25)) >> 26; t2 -= c * ((i64)1 << 26); t3 += c; \ + c = (t6 + ((i64)1<<25)) >> 26; t6 -= c * ((i64)1 << 26); t7 += c; \ + c = (t3 + ((i64)1<<24)) >> 25; t3 -= c * ((i64)1 << 25); t4 += c; \ + c = (t7 + ((i64)1<<24)) >> 25; t7 -= c * ((i64)1 << 25); t8 += c; \ + c = (t4 + ((i64)1<<25)) >> 26; t4 -= c * ((i64)1 << 26); t5 += c; \ + c = (t8 + ((i64)1<<25)) >> 26; t8 -= c * ((i64)1 << 26); t9 += c; \ + c = (t9 + ((i64)1<<24)) >> 25; t9 -= c * ((i64)1 << 25); t0 += c * 19; \ + c = (t0 + ((i64)1<<25)) >> 26; t0 -= c * ((i64)1 << 26); t1 += c; \ + h[0]=(i32)t0; h[1]=(i32)t1; h[2]=(i32)t2; h[3]=(i32)t3; h[4]=(i32)t4; \ + h[5]=(i32)t5; h[6]=(i32)t6; h[7]=(i32)t7; h[8]=(i32)t8; h[9]=(i32)t9 + +// Decodes a field element from a byte buffer. +// mask specifies how many bits we ignore. +// Traditionally we ignore 1. It's useful for EdDSA, +// which uses that bit to denote the sign of x. +// Elligator however uses positive representatives, +// which means ignoring 2 bits instead. +static void fe_frombytes_mask(fe h, const u8 s[32], unsigned nb_mask) +{ + u32 mask = 0xffffff >> nb_mask; + i64 t0 = load32_le(s); // t0 < 2^32 + i64 t1 = load24_le(s + 4) << 6; // t1 < 2^30 + i64 t2 = load24_le(s + 7) << 5; // t2 < 2^29 + i64 t3 = load24_le(s + 10) << 3; // t3 < 2^27 + i64 t4 = load24_le(s + 13) << 2; // t4 < 2^26 + i64 t5 = load32_le(s + 16); // t5 < 2^32 + i64 t6 = load24_le(s + 20) << 7; // t6 < 2^31 + i64 t7 = load24_le(s + 23) << 5; // t7 < 2^29 + i64 t8 = load24_le(s + 26) << 4; // t8 < 2^28 + i64 t9 = (load24_le(s + 29) & mask) << 2; // t9 < 2^25 + FE_CARRY; // Carry precondition OK +} + +static void fe_frombytes(fe h, const u8 s[32]) +{ + fe_frombytes_mask(h, s, 1); +} + + +// Precondition +// |h[0]|, |h[2]|, |h[4]|, |h[6]|, |h[8]| < 1.1 * 2^25 +// |h[1]|, |h[3]|, |h[5]|, |h[7]|, |h[9]| < 1.1 * 2^24 +// +// Therefore, |h| < 2^255-19 +// There are two possibilities: +// +// - If h is positive, all we need to do is reduce its individual +// limbs down to their tight positive range. +// - If h is negative, we also need to add 2^255-19 to it. +// Or just remove 19 and chop off any excess bit. +static void fe_tobytes(u8 s[32], const fe h) +{ + i32 t[10]; + COPY(t, h, 10); + i32 q = (19 * t[9] + (((i32) 1) << 24)) >> 25; + // |t9| < 1.1 * 2^24 + // -1.1 * 2^24 < t9 < 1.1 * 2^24 + // -21 * 2^24 < 19 * t9 < 21 * 2^24 + // -2^29 < 19 * t9 + 2^24 < 2^29 + // -2^29 / 2^25 < (19 * t9 + 2^24) / 2^25 < 2^29 / 2^25 + // -16 < (19 * t9 + 2^24) / 2^25 < 16 + FOR (i, 0, 5) { + q += t[2*i ]; q >>= 26; // q = 0 or -1 + q += t[2*i+1]; q >>= 25; // q = 0 or -1 + } + // q = 0 iff h >= 0 + // q = -1 iff h < 0 + // Adding q * 19 to h reduces h to its proper range. + q *= 19; // Shift carry back to the beginning + FOR (i, 0, 5) { + t[i*2 ] += q; q = t[i*2 ] >> 26; t[i*2 ] -= q * ((i32)1 << 26); + t[i*2+1] += q; q = t[i*2+1] >> 25; t[i*2+1] -= q * ((i32)1 << 25); + } + // h is now fully reduced, and q represents the excess bit. + + store32_le(s + 0, ((u32)t[0] >> 0) | ((u32)t[1] << 26)); + store32_le(s + 4, ((u32)t[1] >> 6) | ((u32)t[2] << 19)); + store32_le(s + 8, ((u32)t[2] >> 13) | ((u32)t[3] << 13)); + store32_le(s + 12, ((u32)t[3] >> 19) | ((u32)t[4] << 6)); + store32_le(s + 16, ((u32)t[5] >> 0) | ((u32)t[6] << 25)); + store32_le(s + 20, ((u32)t[6] >> 7) | ((u32)t[7] << 19)); + store32_le(s + 24, ((u32)t[7] >> 13) | ((u32)t[8] << 12)); + store32_le(s + 28, ((u32)t[8] >> 20) | ((u32)t[9] << 6)); + + WIPE_BUFFER(t); +} + +// Precondition +// ------------- +// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26 +// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25 +// +// |g0|, |g2|, |g4|, |g6|, |g8| < 1.65 * 2^26 +// |g1|, |g3|, |g5|, |g7|, |g9| < 1.65 * 2^25 +static void fe_mul_small(fe h, const fe f, i32 g) +{ + i64 t0 = f[0] * (i64) g; i64 t1 = f[1] * (i64) g; + i64 t2 = f[2] * (i64) g; i64 t3 = f[3] * (i64) g; + i64 t4 = f[4] * (i64) g; i64 t5 = f[5] * (i64) g; + i64 t6 = f[6] * (i64) g; i64 t7 = f[7] * (i64) g; + i64 t8 = f[8] * (i64) g; i64 t9 = f[9] * (i64) g; + // |t0|, |t2|, |t4|, |t6|, |t8| < 1.65 * 2^26 * 2^31 < 2^58 + // |t1|, |t3|, |t5|, |t7|, |t9| < 1.65 * 2^25 * 2^31 < 2^57 + + FE_CARRY; // Carry precondition OK +} + +// Precondition +// ------------- +// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26 +// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25 +// +// |g0|, |g2|, |g4|, |g6|, |g8| < 1.65 * 2^26 +// |g1|, |g3|, |g5|, |g7|, |g9| < 1.65 * 2^25 +static void fe_mul(fe h, const fe f, const fe g) +{ + // Everything is unrolled and put in temporary variables. + // We could roll the loop, but that would make curve25519 twice as slow. + i32 f0 = f[0]; i32 f1 = f[1]; i32 f2 = f[2]; i32 f3 = f[3]; i32 f4 = f[4]; + i32 f5 = f[5]; i32 f6 = f[6]; i32 f7 = f[7]; i32 f8 = f[8]; i32 f9 = f[9]; + i32 g0 = g[0]; i32 g1 = g[1]; i32 g2 = g[2]; i32 g3 = g[3]; i32 g4 = g[4]; + i32 g5 = g[5]; i32 g6 = g[6]; i32 g7 = g[7]; i32 g8 = g[8]; i32 g9 = g[9]; + i32 F1 = f1*2; i32 F3 = f3*2; i32 F5 = f5*2; i32 F7 = f7*2; i32 F9 = f9*2; + i32 G1 = g1*19; i32 G2 = g2*19; i32 G3 = g3*19; + i32 G4 = g4*19; i32 G5 = g5*19; i32 G6 = g6*19; + i32 G7 = g7*19; i32 G8 = g8*19; i32 G9 = g9*19; + // |F1|, |F3|, |F5|, |F7|, |F9| < 1.65 * 2^26 + // |G0|, |G2|, |G4|, |G6|, |G8| < 2^31 + // |G1|, |G3|, |G5|, |G7|, |G9| < 2^30 + + i64 t0 = f0*(i64)g0 + F1*(i64)G9 + f2*(i64)G8 + F3*(i64)G7 + f4*(i64)G6 + + F5*(i64)G5 + f6*(i64)G4 + F7*(i64)G3 + f8*(i64)G2 + F9*(i64)G1; + i64 t1 = f0*(i64)g1 + f1*(i64)g0 + f2*(i64)G9 + f3*(i64)G8 + f4*(i64)G7 + + f5*(i64)G6 + f6*(i64)G5 + f7*(i64)G4 + f8*(i64)G3 + f9*(i64)G2; + i64 t2 = f0*(i64)g2 + F1*(i64)g1 + f2*(i64)g0 + F3*(i64)G9 + f4*(i64)G8 + + F5*(i64)G7 + f6*(i64)G6 + F7*(i64)G5 + f8*(i64)G4 + F9*(i64)G3; + i64 t3 = f0*(i64)g3 + f1*(i64)g2 + f2*(i64)g1 + f3*(i64)g0 + f4*(i64)G9 + + f5*(i64)G8 + f6*(i64)G7 + f7*(i64)G6 + f8*(i64)G5 + f9*(i64)G4; + i64 t4 = f0*(i64)g4 + F1*(i64)g3 + f2*(i64)g2 + F3*(i64)g1 + f4*(i64)g0 + + F5*(i64)G9 + f6*(i64)G8 + F7*(i64)G7 + f8*(i64)G6 + F9*(i64)G5; + i64 t5 = f0*(i64)g5 + f1*(i64)g4 + f2*(i64)g3 + f3*(i64)g2 + f4*(i64)g1 + + f5*(i64)g0 + f6*(i64)G9 + f7*(i64)G8 + f8*(i64)G7 + f9*(i64)G6; + i64 t6 = f0*(i64)g6 + F1*(i64)g5 + f2*(i64)g4 + F3*(i64)g3 + f4*(i64)g2 + + F5*(i64)g1 + f6*(i64)g0 + F7*(i64)G9 + f8*(i64)G8 + F9*(i64)G7; + i64 t7 = f0*(i64)g7 + f1*(i64)g6 + f2*(i64)g5 + f3*(i64)g4 + f4*(i64)g3 + + f5*(i64)g2 + f6*(i64)g1 + f7*(i64)g0 + f8*(i64)G9 + f9*(i64)G8; + i64 t8 = f0*(i64)g8 + F1*(i64)g7 + f2*(i64)g6 + F3*(i64)g5 + f4*(i64)g4 + + F5*(i64)g3 + f6*(i64)g2 + F7*(i64)g1 + f8*(i64)g0 + F9*(i64)G9; + i64 t9 = f0*(i64)g9 + f1*(i64)g8 + f2*(i64)g7 + f3*(i64)g6 + f4*(i64)g5 + + f5*(i64)g4 + f6*(i64)g3 + f7*(i64)g2 + f8*(i64)g1 + f9*(i64)g0; + // t0 < 0.67 * 2^61 + // t1 < 0.41 * 2^61 + // t2 < 0.52 * 2^61 + // t3 < 0.32 * 2^61 + // t4 < 0.38 * 2^61 + // t5 < 0.22 * 2^61 + // t6 < 0.23 * 2^61 + // t7 < 0.13 * 2^61 + // t8 < 0.09 * 2^61 + // t9 < 0.03 * 2^61 + + FE_CARRY; // Everything below 2^62, Carry precondition OK +} + +// Precondition +// ------------- +// |f0|, |f2|, |f4|, |f6|, |f8| < 1.65 * 2^26 +// |f1|, |f3|, |f5|, |f7|, |f9| < 1.65 * 2^25 +// +// Note: we could use fe_mul() for this, but this is significantly faster +static void fe_sq(fe h, const fe f) +{ + i32 f0 = f[0]; i32 f1 = f[1]; i32 f2 = f[2]; i32 f3 = f[3]; i32 f4 = f[4]; + i32 f5 = f[5]; i32 f6 = f[6]; i32 f7 = f[7]; i32 f8 = f[8]; i32 f9 = f[9]; + i32 f0_2 = f0*2; i32 f1_2 = f1*2; i32 f2_2 = f2*2; i32 f3_2 = f3*2; + i32 f4_2 = f4*2; i32 f5_2 = f5*2; i32 f6_2 = f6*2; i32 f7_2 = f7*2; + i32 f5_38 = f5*38; i32 f6_19 = f6*19; i32 f7_38 = f7*38; + i32 f8_19 = f8*19; i32 f9_38 = f9*38; + // |f0_2| , |f2_2| , |f4_2| , |f6_2| , |f8_2| < 1.65 * 2^27 + // |f1_2| , |f3_2| , |f5_2| , |f7_2| , |f9_2| < 1.65 * 2^26 + // |f5_38|, |f6_19|, |f7_38|, |f8_19|, |f9_38| < 2^31 + + i64 t0 = f0 *(i64)f0 + f1_2*(i64)f9_38 + f2_2*(i64)f8_19 + + f3_2*(i64)f7_38 + f4_2*(i64)f6_19 + f5 *(i64)f5_38; + i64 t1 = f0_2*(i64)f1 + f2 *(i64)f9_38 + f3_2*(i64)f8_19 + + f4 *(i64)f7_38 + f5_2*(i64)f6_19; + i64 t2 = f0_2*(i64)f2 + f1_2*(i64)f1 + f3_2*(i64)f9_38 + + f4_2*(i64)f8_19 + f5_2*(i64)f7_38 + f6 *(i64)f6_19; + i64 t3 = f0_2*(i64)f3 + f1_2*(i64)f2 + f4 *(i64)f9_38 + + f5_2*(i64)f8_19 + f6 *(i64)f7_38; + i64 t4 = f0_2*(i64)f4 + f1_2*(i64)f3_2 + f2 *(i64)f2 + + f5_2*(i64)f9_38 + f6_2*(i64)f8_19 + f7 *(i64)f7_38; + i64 t5 = f0_2*(i64)f5 + f1_2*(i64)f4 + f2_2*(i64)f3 + + f6 *(i64)f9_38 + f7_2*(i64)f8_19; + i64 t6 = f0_2*(i64)f6 + f1_2*(i64)f5_2 + f2_2*(i64)f4 + + f3_2*(i64)f3 + f7_2*(i64)f9_38 + f8 *(i64)f8_19; + i64 t7 = f0_2*(i64)f7 + f1_2*(i64)f6 + f2_2*(i64)f5 + + f3_2*(i64)f4 + f8 *(i64)f9_38; + i64 t8 = f0_2*(i64)f8 + f1_2*(i64)f7_2 + f2_2*(i64)f6 + + f3_2*(i64)f5_2 + f4 *(i64)f4 + f9 *(i64)f9_38; + i64 t9 = f0_2*(i64)f9 + f1_2*(i64)f8 + f2_2*(i64)f7 + + f3_2*(i64)f6 + f4 *(i64)f5_2; + // t0 < 0.67 * 2^61 + // t1 < 0.41 * 2^61 + // t2 < 0.52 * 2^61 + // t3 < 0.32 * 2^61 + // t4 < 0.38 * 2^61 + // t5 < 0.22 * 2^61 + // t6 < 0.23 * 2^61 + // t7 < 0.13 * 2^61 + // t8 < 0.09 * 2^61 + // t9 < 0.03 * 2^61 + + FE_CARRY; +} + +// Parity check. Returns 0 if even, 1 if odd +static int fe_isodd(const fe f) +{ + u8 s[32]; + fe_tobytes(s, f); + u8 isodd = s[0] & 1; + WIPE_BUFFER(s); + return isodd; +} + +// Returns 1 if equal, 0 if not equal +static int fe_isequal(const fe f, const fe g) +{ + u8 fs[32]; + u8 gs[32]; + fe_tobytes(fs, f); + fe_tobytes(gs, g); + int isdifferent = crypto_verify32(fs, gs); + WIPE_BUFFER(fs); + WIPE_BUFFER(gs); + return 1 + isdifferent; +} + +// Inverse square root. +// Returns true if x is a square, false otherwise. +// After the call: +// isr = sqrt(1/x) if x is a non-zero square. +// isr = sqrt(sqrt(-1)/x) if x is not a square. +// isr = 0 if x is zero. +// We do not guarantee the sign of the square root. +// +// Notes: +// Let quartic = x^((p-1)/4) +// +// x^((p-1)/2) = chi(x) +// quartic^2 = chi(x) +// quartic = sqrt(chi(x)) +// quartic = 1 or -1 or sqrt(-1) or -sqrt(-1) +// +// Note that x is a square if quartic is 1 or -1 +// There are 4 cases to consider: +// +// if quartic = 1 (x is a square) +// then x^((p-1)/4) = 1 +// x^((p-5)/4) * x = 1 +// x^((p-5)/4) = 1/x +// x^((p-5)/8) = sqrt(1/x) or -sqrt(1/x) +// +// if quartic = -1 (x is a square) +// then x^((p-1)/4) = -1 +// x^((p-5)/4) * x = -1 +// x^((p-5)/4) = -1/x +// x^((p-5)/8) = sqrt(-1) / sqrt(x) +// x^((p-5)/8) * sqrt(-1) = sqrt(-1)^2 / sqrt(x) +// x^((p-5)/8) * sqrt(-1) = -1/sqrt(x) +// x^((p-5)/8) * sqrt(-1) = -sqrt(1/x) or sqrt(1/x) +// +// if quartic = sqrt(-1) (x is not a square) +// then x^((p-1)/4) = sqrt(-1) +// x^((p-5)/4) * x = sqrt(-1) +// x^((p-5)/4) = sqrt(-1)/x +// x^((p-5)/8) = sqrt(sqrt(-1)/x) or -sqrt(sqrt(-1)/x) +// +// Note that the product of two non-squares is always a square: +// For any non-squares a and b, chi(a) = -1 and chi(b) = -1. +// Since chi(x) = x^((p-1)/2), chi(a)*chi(b) = chi(a*b) = 1. +// Therefore a*b is a square. +// +// Since sqrt(-1) and x are both non-squares, their product is a +// square, and we can compute their square root. +// +// if quartic = -sqrt(-1) (x is not a square) +// then x^((p-1)/4) = -sqrt(-1) +// x^((p-5)/4) * x = -sqrt(-1) +// x^((p-5)/4) = -sqrt(-1)/x +// x^((p-5)/8) = sqrt(-sqrt(-1)/x) +// x^((p-5)/8) = sqrt( sqrt(-1)/x) * sqrt(-1) +// x^((p-5)/8) * sqrt(-1) = sqrt( sqrt(-1)/x) * sqrt(-1)^2 +// x^((p-5)/8) * sqrt(-1) = sqrt( sqrt(-1)/x) * -1 +// x^((p-5)/8) * sqrt(-1) = -sqrt(sqrt(-1)/x) or sqrt(sqrt(-1)/x) +static int invsqrt(fe isr, const fe x) +{ + fe t0, t1, t2; + + // t0 = x^((p-5)/8) + // Can be achieved with a simple double & add ladder, + // but it would be slower. + fe_sq(t0, x); + fe_sq(t1,t0); fe_sq(t1, t1); fe_mul(t1, x, t1); + fe_mul(t0, t0, t1); + fe_sq(t0, t0); fe_mul(t0, t1, t0); + fe_sq(t1, t0); FOR (i, 1, 5) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); + fe_sq(t1, t0); FOR (i, 1, 10) { fe_sq(t1, t1); } fe_mul(t1, t1, t0); + fe_sq(t2, t1); FOR (i, 1, 20) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); + fe_sq(t1, t1); FOR (i, 1, 10) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); + fe_sq(t1, t0); FOR (i, 1, 50) { fe_sq(t1, t1); } fe_mul(t1, t1, t0); + fe_sq(t2, t1); FOR (i, 1, 100) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); + fe_sq(t1, t1); FOR (i, 1, 50) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); + fe_sq(t0, t0); FOR (i, 1, 2) { fe_sq(t0, t0); } fe_mul(t0, t0, x); + + // quartic = x^((p-1)/4) + i32 *quartic = t1; + fe_sq (quartic, t0); + fe_mul(quartic, quartic, x); + + i32 *check = t2; + fe_0 (check); int z0 = fe_isequal(x , check); + fe_1 (check); int p1 = fe_isequal(quartic, check); + fe_neg(check, check ); int m1 = fe_isequal(quartic, check); + fe_neg(check, sqrtm1); int ms = fe_isequal(quartic, check); + + // if quartic == -1 or sqrt(-1) + // then isr = x^((p-1)/4) * sqrt(-1) + // else isr = x^((p-1)/4) + fe_mul(isr, t0, sqrtm1); + fe_ccopy(isr, t0, 1 - (m1 | ms)); + + WIPE_BUFFER(t0); + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); + return p1 | m1 | z0; +} + +// Inverse in terms of inverse square root. +// Requires two additional squarings to get rid of the sign. +// +// 1/x = x * (+invsqrt(x^2))^2 +// = x * (-invsqrt(x^2))^2 +// +// A fully optimised exponentiation by p-1 would save 6 field +// multiplications, but it would require more code. +static void fe_invert(fe out, const fe x) +{ + fe tmp; + fe_sq(tmp, x); + invsqrt(tmp, tmp); + fe_sq(tmp, tmp); + fe_mul(out, tmp, x); + WIPE_BUFFER(tmp); +} + +// trim a scalar for scalar multiplication +void crypto_eddsa_trim_scalar(u8 out[32], const u8 in[32]) +{ + COPY(out, in, 32); + out[ 0] &= 248; + out[31] &= 127; + out[31] |= 64; +} + +// get bit from scalar at position i +static int scalar_bit(const u8 s[32], int i) +{ + if (i < 0) { return 0; } // handle -1 for sliding windows + return (s[i>>3] >> (i&7)) & 1; +} + +/////////////// +/// X-25519 /// Taken from SUPERCOP's ref10 implementation. +/////////////// +static void scalarmult(u8 q[32], const u8 scalar[32], const u8 p[32], + int nb_bits) +{ + // computes the scalar product + fe x1; + fe_frombytes(x1, p); + + // computes the actual scalar product (the result is in x2 and z2) + fe x2, z2, x3, z3, t0, t1; + // Montgomery ladder + // In projective coordinates, to avoid divisions: x = X / Z + // We don't care about the y coordinate, it's only 1 bit of information + fe_1(x2); fe_0(z2); // "zero" point + fe_copy(x3, x1); fe_1(z3); // "one" point + int swap = 0; + for (int pos = nb_bits-1; pos >= 0; --pos) { + // constant time conditional swap before ladder step + int b = scalar_bit(scalar, pos); + swap ^= b; // xor trick avoids swapping at the end of the loop + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + swap = b; // anticipates one last swap after the loop + + // Montgomery ladder step: replaces (P2, P3) by (P2*2, P2+P3) + // with differential addition + fe_sub(t0, x3, z3); + fe_sub(t1, x2, z2); + fe_add(x2, x2, z2); + fe_add(z2, x3, z3); + fe_mul(z3, t0, x2); + fe_mul(z2, z2, t1); + fe_sq (t0, t1 ); + fe_sq (t1, x2 ); + fe_add(x3, z3, z2); + fe_sub(z2, z3, z2); + fe_mul(x2, t1, t0); + fe_sub(t1, t1, t0); + fe_sq (z2, z2 ); + fe_mul_small(z3, t1, 121666); + fe_sq (x3, x3 ); + fe_add(t0, t0, z3); + fe_mul(z3, x1, z2); + fe_mul(z2, t1, t0); + } + // last swap is necessary to compensate for the xor trick + // Note: after this swap, P3 == P2 + P1. + fe_cswap(x2, x3, swap); + fe_cswap(z2, z3, swap); + + // normalises the coordinates: x == X / Z + fe_invert(z2, z2); + fe_mul(x2, x2, z2); + fe_tobytes(q, x2); + + WIPE_BUFFER(x1); + WIPE_BUFFER(x2); WIPE_BUFFER(z2); WIPE_BUFFER(t0); + WIPE_BUFFER(x3); WIPE_BUFFER(z3); WIPE_BUFFER(t1); +} + +void crypto_x25519(u8 raw_shared_secret[32], + const u8 your_secret_key [32], + const u8 their_public_key [32]) +{ + // restrict the possible scalar values + u8 e[32]; + crypto_eddsa_trim_scalar(e, your_secret_key); + scalarmult(raw_shared_secret, e, their_public_key, 255); + WIPE_BUFFER(e); +} + +void crypto_x25519_public_key(u8 public_key[32], + const u8 secret_key[32]) +{ + static const u8 base_point[32] = {9}; + crypto_x25519(public_key, secret_key, base_point); +} + +/////////////////////////// +/// Arithmetic modulo L /// +/////////////////////////// +static const u32 L[8] = { + 0x5cf5d3ed, 0x5812631a, 0xa2f79cd6, 0x14def9de, + 0x00000000, 0x00000000, 0x00000000, 0x10000000, +}; + +// p = a*b + p +static void multiply(u32 p[16], const u32 a[8], const u32 b[8]) +{ + FOR (i, 0, 8) { + u64 carry = 0; + FOR (j, 0, 8) { + carry += p[i+j] + (u64)a[i] * b[j]; + p[i+j] = (u32)carry; + carry >>= 32; + } + p[i+8] = (u32)carry; + } +} + +static int is_above_l(const u32 x[8]) +{ + // We work with L directly, in a 2's complement encoding + // (-L == ~L + 1) + u64 carry = 1; + FOR (i, 0, 8) { + carry += (u64)x[i] + (~L[i] & 0xffffffff); + carry >>= 32; + } + return (int)carry; // carry is either 0 or 1 +} + +// Final reduction modulo L, by conditionally removing L. +// if x < l , then r = x +// if l <= x 2*l, then r = x-l +// otherwise the result will be wrong +static void remove_l(u32 r[8], const u32 x[8]) +{ + u64 carry = (u64)is_above_l(x); + u32 mask = ~(u32)carry + 1; // carry == 0 or 1 + FOR (i, 0, 8) { + carry += (u64)x[i] + (~L[i] & mask); + r[i] = (u32)carry; + carry >>= 32; + } +} + +// Full reduction modulo L (Barrett reduction) +static void mod_l(u8 reduced[32], const u32 x[16]) +{ + static const u32 r[9] = { + 0x0a2c131b,0xed9ce5a3,0x086329a7,0x2106215d, + 0xffffffeb,0xffffffff,0xffffffff,0xffffffff,0xf, + }; + // xr = x * r + u32 xr[25] = {0}; + FOR (i, 0, 9) { + u64 carry = 0; + FOR (j, 0, 16) { + carry += xr[i+j] + (u64)r[i] * x[j]; + xr[i+j] = (u32)carry; + carry >>= 32; + } + xr[i+16] = (u32)carry; + } + // xr = floor(xr / 2^512) * L + // Since the result is guaranteed to be below 2*L, + // it is enough to only compute the first 256 bits. + // The division is performed by saying xr[i+16]. (16 * 32 = 512) + ZERO(xr, 8); + FOR (i, 0, 8) { + u64 carry = 0; + FOR (j, 0, 8-i) { + carry += xr[i+j] + (u64)xr[i+16] * L[j]; + xr[i+j] = (u32)carry; + carry >>= 32; + } + } + // xr = x - xr + u64 carry = 1; + FOR (i, 0, 8) { + carry += (u64)x[i] + (~xr[i] & 0xffffffff); + xr[i] = (u32)carry; + carry >>= 32; + } + // Final reduction modulo L (conditional subtraction) + remove_l(xr, xr); + store32_le_buf(reduced, xr, 8); + + WIPE_BUFFER(xr); +} + +void crypto_eddsa_reduce(u8 reduced[32], const u8 expanded[64]) +{ + u32 x[16]; + load32_le_buf(x, expanded, 16); + mod_l(reduced, x); + WIPE_BUFFER(x); +} + +// r = (a * b) + c +void crypto_eddsa_mul_add(u8 r[32], + const u8 a[32], const u8 b[32], const u8 c[32]) +{ + u32 A[8]; load32_le_buf(A, a, 8); + u32 B[8]; load32_le_buf(B, b, 8); + u32 p[16]; load32_le_buf(p, c, 8); ZERO(p + 8, 8); + multiply(p, A, B); + mod_l(r, p); + WIPE_BUFFER(p); + WIPE_BUFFER(A); + WIPE_BUFFER(B); +} + +/////////////// +/// Ed25519 /// +/////////////// + +// Point (group element, ge) in a twisted Edwards curve, +// in extended projective coordinates. +// ge : x = X/Z, y = Y/Z, T = XY/Z +// ge_cached : Yp = X+Y, Ym = X-Y, T2 = T*D2 +// ge_precomp: Z = 1 +typedef struct { fe X; fe Y; fe Z; fe T; } ge; +typedef struct { fe Yp; fe Ym; fe Z; fe T2; } ge_cached; +typedef struct { fe Yp; fe Ym; fe T2; } ge_precomp; + +static void ge_zero(ge *p) +{ + fe_0(p->X); + fe_1(p->Y); + fe_1(p->Z); + fe_0(p->T); +} + +static void ge_tobytes(u8 s[32], const ge *h) +{ + fe recip, x, y; + fe_invert(recip, h->Z); + fe_mul(x, h->X, recip); + fe_mul(y, h->Y, recip); + fe_tobytes(s, y); + s[31] ^= fe_isodd(x) << 7; + + WIPE_BUFFER(recip); + WIPE_BUFFER(x); + WIPE_BUFFER(y); +} + +// h = -s, where s is a point encoded in 32 bytes +// +// Variable time! Inputs must not be secret! +// => Use only to *check* signatures. +// +// From the specifications: +// The encoding of s contains y and the sign of x +// x = sqrt((y^2 - 1) / (d*y^2 + 1)) +// In extended coordinates: +// X = x, Y = y, Z = 1, T = x*y +// +// Note that num * den is a square iff num / den is a square +// If num * den is not a square, the point was not on the curve. +// From the above: +// Let num = y^2 - 1 +// Let den = d*y^2 + 1 +// x = sqrt((y^2 - 1) / (d*y^2 + 1)) +// x = sqrt(num / den) +// x = sqrt(num^2 / (num * den)) +// x = num * sqrt(1 / (num * den)) +// +// Therefore, we can just compute: +// num = y^2 - 1 +// den = d*y^2 + 1 +// isr = invsqrt(num * den) // abort if not square +// x = num * isr +// Finally, negate x if its sign is not as specified. +static int ge_frombytes_neg_vartime(ge *h, const u8 s[32]) +{ + fe_frombytes(h->Y, s); + fe_1(h->Z); + fe_sq (h->T, h->Y); // t = y^2 + fe_mul(h->X, h->T, d ); // x = d*y^2 + fe_sub(h->T, h->T, h->Z); // t = y^2 - 1 + fe_add(h->X, h->X, h->Z); // x = d*y^2 + 1 + fe_mul(h->X, h->T, h->X); // x = (y^2 - 1) * (d*y^2 + 1) + int is_square = invsqrt(h->X, h->X); + if (!is_square) { + return -1; // Not on the curve, abort + } + fe_mul(h->X, h->T, h->X); // x = sqrt((y^2 - 1) / (d*y^2 + 1)) + if (fe_isodd(h->X) == (s[31] >> 7)) { + fe_neg(h->X, h->X); + } + fe_mul(h->T, h->X, h->Y); + return 0; +} + +static void ge_cache(ge_cached *c, const ge *p) +{ + fe_add (c->Yp, p->Y, p->X); + fe_sub (c->Ym, p->Y, p->X); + fe_copy(c->Z , p->Z ); + fe_mul (c->T2, p->T, D2 ); +} + +// Internal buffers are not wiped! Inputs must not be secret! +// => Use only to *check* signatures. +static void ge_add(ge *s, const ge *p, const ge_cached *q) +{ + fe a, b; + fe_add(a , p->Y, p->X ); + fe_sub(b , p->Y, p->X ); + fe_mul(a , a , q->Yp); + fe_mul(b , b , q->Ym); + fe_add(s->Y, a , b ); + fe_sub(s->X, a , b ); + + fe_add(s->Z, p->Z, p->Z ); + fe_mul(s->Z, s->Z, q->Z ); + fe_mul(s->T, p->T, q->T2); + fe_add(a , s->Z, s->T ); + fe_sub(b , s->Z, s->T ); + + fe_mul(s->T, s->X, s->Y); + fe_mul(s->X, s->X, b ); + fe_mul(s->Y, s->Y, a ); + fe_mul(s->Z, a , b ); +} + +// Internal buffers are not wiped! Inputs must not be secret! +// => Use only to *check* signatures. +static void ge_sub(ge *s, const ge *p, const ge_cached *q) +{ + ge_cached neg; + fe_copy(neg.Ym, q->Yp); + fe_copy(neg.Yp, q->Ym); + fe_copy(neg.Z , q->Z ); + fe_neg (neg.T2, q->T2); + ge_add(s, p, &neg); +} + +static void ge_madd(ge *s, const ge *p, const ge_precomp *q, fe a, fe b) +{ + fe_add(a , p->Y, p->X ); + fe_sub(b , p->Y, p->X ); + fe_mul(a , a , q->Yp); + fe_mul(b , b , q->Ym); + fe_add(s->Y, a , b ); + fe_sub(s->X, a , b ); + + fe_add(s->Z, p->Z, p->Z ); + fe_mul(s->T, p->T, q->T2); + fe_add(a , s->Z, s->T ); + fe_sub(b , s->Z, s->T ); + + fe_mul(s->T, s->X, s->Y); + fe_mul(s->X, s->X, b ); + fe_mul(s->Y, s->Y, a ); + fe_mul(s->Z, a , b ); +} + +// Internal buffers are not wiped! Inputs must not be secret! +// => Use only to *check* signatures. +static void ge_msub(ge *s, const ge *p, const ge_precomp *q, fe a, fe b) +{ + ge_precomp neg; + fe_copy(neg.Ym, q->Yp); + fe_copy(neg.Yp, q->Ym); + fe_neg (neg.T2, q->T2); + ge_madd(s, p, &neg, a, b); +} + +static void ge_double(ge *s, const ge *p, ge *q) +{ + fe_sq (q->X, p->X); + fe_sq (q->Y, p->Y); + fe_sq (q->Z, p->Z); // qZ = pZ^2 + fe_mul_small(q->Z, q->Z, 2); // qZ = pZ^2 * 2 + fe_add(q->T, p->X, p->Y); + fe_sq (s->T, q->T); + fe_add(q->T, q->Y, q->X); + fe_sub(q->Y, q->Y, q->X); + fe_sub(q->X, s->T, q->T); + fe_sub(q->Z, q->Z, q->Y); + + fe_mul(s->X, q->X , q->Z); + fe_mul(s->Y, q->T , q->Y); + fe_mul(s->Z, q->Y , q->Z); + fe_mul(s->T, q->X , q->T); +} + +// 5-bit signed window in cached format (Niels coordinates, Z=1) +static const ge_precomp b_window[8] = { + {{25967493,-14356035,29566456,3660896,-12694345, + 4014787,27544626,-11754271,-6079156,2047605,}, + {-12545711,934262,-2722910,3049990,-727428, + 9406986,12720692,5043384,19500929,-15469378,}, + {-8738181,4489570,9688441,-14785194,10184609, + -12363380,29287919,11864899,-24514362,-4438546,},}, + {{15636291,-9688557,24204773,-7912398,616977, + -16685262,27787600,-14772189,28944400,-1550024,}, + {16568933,4717097,-11556148,-1102322,15682896, + -11807043,16354577,-11775962,7689662,11199574,}, + {30464156,-5976125,-11779434,-15670865,23220365, + 15915852,7512774,10017326,-17749093,-9920357,},}, + {{10861363,11473154,27284546,1981175,-30064349, + 12577861,32867885,14515107,-15438304,10819380,}, + {4708026,6336745,20377586,9066809,-11272109, + 6594696,-25653668,12483688,-12668491,5581306,}, + {19563160,16186464,-29386857,4097519,10237984, + -4348115,28542350,13850243,-23678021,-15815942,},}, + {{5153746,9909285,1723747,-2777874,30523605, + 5516873,19480852,5230134,-23952439,-15175766,}, + {-30269007,-3463509,7665486,10083793,28475525, + 1649722,20654025,16520125,30598449,7715701,}, + {28881845,14381568,9657904,3680757,-20181635, + 7843316,-31400660,1370708,29794553,-1409300,},}, + {{-22518993,-6692182,14201702,-8745502,-23510406, + 8844726,18474211,-1361450,-13062696,13821877,}, + {-6455177,-7839871,3374702,-4740862,-27098617, + -10571707,31655028,-7212327,18853322,-14220951,}, + {4566830,-12963868,-28974889,-12240689,-7602672, + -2830569,-8514358,-10431137,2207753,-3209784,},}, + {{-25154831,-4185821,29681144,7868801,-6854661, + -9423865,-12437364,-663000,-31111463,-16132436,}, + {25576264,-2703214,7349804,-11814844,16472782, + 9300885,3844789,15725684,171356,6466918,}, + {23103977,13316479,9739013,-16149481,817875, + -15038942,8965339,-14088058,-30714912,16193877,},}, + {{-33521811,3180713,-2394130,14003687,-16903474, + -16270840,17238398,4729455,-18074513,9256800,}, + {-25182317,-4174131,32336398,5036987,-21236817, + 11360617,22616405,9761698,-19827198,630305,}, + {-13720693,2639453,-24237460,-7406481,9494427, + -5774029,-6554551,-15960994,-2449256,-14291300,},}, + {{-3151181,-5046075,9282714,6866145,-31907062, + -863023,-18940575,15033784,25105118,-7894876,}, + {-24326370,15950226,-31801215,-14592823,-11662737, + -5090925,1573892,-2625887,2198790,-15804619,}, + {-3099351,10324967,-2241613,7453183,-5446979, + -2735503,-13812022,-16236442,-32461234,-12290683,},}, +}; + +// Incremental sliding windows (left to right) +// Based on Roberto Maria Avanzi[2005] +typedef struct { + i16 next_index; // position of the next signed digit + i8 next_digit; // next signed digit (odd number below 2^window_width) + u8 next_check; // point at which we must check for a new window +} slide_ctx; + +static void slide_init(slide_ctx *ctx, const u8 scalar[32]) +{ + // scalar is guaranteed to be below L, either because we checked (s), + // or because we reduced it modulo L (h_ram). L is under 2^253, so + // so bits 253 to 255 are guaranteed to be zero. No need to test them. + // + // Note however that L is very close to 2^252, so bit 252 is almost + // always zero. If we were to start at bit 251, the tests wouldn't + // catch the off-by-one error (constructing one that does would be + // prohibitively expensive). + // + // We should still check bit 252, though. + int i = 252; + while (i > 0 && scalar_bit(scalar, i) == 0) { + i--; + } + ctx->next_check = (u8)(i + 1); + ctx->next_index = -1; + ctx->next_digit = -1; +} + +static int slide_step(slide_ctx *ctx, int width, int i, const u8 scalar[32]) +{ + if (i == ctx->next_check) { + if (scalar_bit(scalar, i) == scalar_bit(scalar, i - 1)) { + ctx->next_check--; + } else { + // compute digit of next window + int w = MIN(width, i + 1); + int v = -(scalar_bit(scalar, i) << (w-1)); + FOR_T (int, j, 0, w-1) { + v += scalar_bit(scalar, i-(w-1)+j) << j; + } + v += scalar_bit(scalar, i-w); + int lsb = v & (~v + 1); // smallest bit of v + int s = // log2(lsb) + (((lsb & 0xAA) != 0) << 0) | + (((lsb & 0xCC) != 0) << 1) | + (((lsb & 0xF0) != 0) << 2); + ctx->next_index = (i16)(i-(w-1)+s); + ctx->next_digit = (i8) (v >> s ); + ctx->next_check -= (u8) w; + } + } + return i == ctx->next_index ? ctx->next_digit: 0; +} + +#define P_W_WIDTH 3 // Affects the size of the stack +#define B_W_WIDTH 5 // Affects the size of the binary +#define P_W_SIZE (1<<(P_W_WIDTH-2)) + +int crypto_eddsa_check_equation(const u8 signature[64], const u8 public_key[32], + const u8 h[32]) +{ + ge minus_A; // -public_key + ge minus_R; // -first_half_of_signature + const u8 *s = signature + 32; + + // Check that A and R are on the curve + // Check that 0 <= S < L (prevents malleability) + // *Allow* non-cannonical encoding for A and R + { + u32 s32[8]; + load32_le_buf(s32, s, 8); + if (ge_frombytes_neg_vartime(&minus_A, public_key) || + ge_frombytes_neg_vartime(&minus_R, signature) || + is_above_l(s32)) { + return -1; + } + } + + // look-up table for minus_A + ge_cached lutA[P_W_SIZE]; + { + ge minus_A2, tmp; + ge_double(&minus_A2, &minus_A, &tmp); + ge_cache(&lutA[0], &minus_A); + FOR (i, 1, P_W_SIZE) { + ge_add(&tmp, &minus_A2, &lutA[i-1]); + ge_cache(&lutA[i], &tmp); + } + } + + // sum = [s]B - [h]A + // Merged double and add ladder, fused with sliding + slide_ctx h_slide; slide_init(&h_slide, h); + slide_ctx s_slide; slide_init(&s_slide, s); + int i = MAX(h_slide.next_check, s_slide.next_check); + ge *sum = &minus_A; // reuse minus_A for the sum + ge_zero(sum); + while (i >= 0) { + ge tmp; + ge_double(sum, sum, &tmp); + int h_digit = slide_step(&h_slide, P_W_WIDTH, i, h); + int s_digit = slide_step(&s_slide, B_W_WIDTH, i, s); + if (h_digit > 0) { ge_add(sum, sum, &lutA[ h_digit / 2]); } + if (h_digit < 0) { ge_sub(sum, sum, &lutA[-h_digit / 2]); } + fe t1, t2; + if (s_digit > 0) { ge_madd(sum, sum, b_window + s_digit/2, t1, t2); } + if (s_digit < 0) { ge_msub(sum, sum, b_window + -s_digit/2, t1, t2); } + i--; + } + + // Compare [8](sum-R) and the zero point + // The multiplication by 8 eliminates any low-order component + // and ensures consistency with batched verification. + ge_cached cached; + u8 check[32]; + static const u8 zero_point[32] = {1}; // Point of order 1 + ge_cache(&cached, &minus_R); + ge_add(sum, sum, &cached); + ge_double(sum, sum, &minus_R); // reuse minus_R as temporary + ge_double(sum, sum, &minus_R); // reuse minus_R as temporary + ge_double(sum, sum, &minus_R); // reuse minus_R as temporary + ge_tobytes(check, sum); + return crypto_verify32(check, zero_point); +} + +// 5-bit signed comb in cached format (Niels coordinates, Z=1) +static const ge_precomp b_comb_low[8] = { + {{-6816601,-2324159,-22559413,124364,18015490, + 8373481,19993724,1979872,-18549925,9085059,}, + {10306321,403248,14839893,9633706,8463310, + -8354981,-14305673,14668847,26301366,2818560,}, + {-22701500,-3210264,-13831292,-2927732,-16326337, + -14016360,12940910,177905,12165515,-2397893,},}, + {{-12282262,-7022066,9920413,-3064358,-32147467, + 2927790,22392436,-14852487,2719975,16402117,}, + {-7236961,-4729776,2685954,-6525055,-24242706, + -15940211,-6238521,14082855,10047669,12228189,}, + {-30495588,-12893761,-11161261,3539405,-11502464, + 16491580,-27286798,-15030530,-7272871,-15934455,},}, + {{17650926,582297,-860412,-187745,-12072900, + -10683391,-20352381,15557840,-31072141,-5019061,}, + {-6283632,-2259834,-4674247,-4598977,-4089240, + 12435688,-31278303,1060251,6256175,10480726,}, + {-13871026,2026300,-21928428,-2741605,-2406664, + -8034988,7355518,15733500,-23379862,7489131,},}, + {{6883359,695140,23196907,9644202,-33430614, + 11354760,-20134606,6388313,-8263585,-8491918,}, + {-7716174,-13605463,-13646110,14757414,-19430591, + -14967316,10359532,-11059670,-21935259,12082603,}, + {-11253345,-15943946,10046784,5414629,24840771, + 8086951,-6694742,9868723,15842692,-16224787,},}, + {{9639399,11810955,-24007778,-9320054,3912937, + -9856959,996125,-8727907,-8919186,-14097242,}, + {7248867,14468564,25228636,-8795035,14346339, + 8224790,6388427,-7181107,6468218,-8720783,}, + {15513115,15439095,7342322,-10157390,18005294, + -7265713,2186239,4884640,10826567,7135781,},}, + {{-14204238,5297536,-5862318,-6004934,28095835, + 4236101,-14203318,1958636,-16816875,3837147,}, + {-5511166,-13176782,-29588215,12339465,15325758, + -15945770,-8813185,11075932,-19608050,-3776283,}, + {11728032,9603156,-4637821,-5304487,-7827751, + 2724948,31236191,-16760175,-7268616,14799772,},}, + {{-28842672,4840636,-12047946,-9101456,-1445464, + 381905,-30977094,-16523389,1290540,12798615,}, + {27246947,-10320914,14792098,-14518944,5302070, + -8746152,-3403974,-4149637,-27061213,10749585,}, + {25572375,-6270368,-15353037,16037944,1146292, + 32198,23487090,9585613,24714571,-1418265,},}, + {{19844825,282124,-17583147,11004019,-32004269, + -2716035,6105106,-1711007,-21010044,14338445,}, + {8027505,8191102,-18504907,-12335737,25173494, + -5923905,15446145,7483684,-30440441,10009108,}, + {-14134701,-4174411,10246585,-14677495,33553567, + -14012935,23366126,15080531,-7969992,7663473,},}, +}; + +static const ge_precomp b_comb_high[8] = { + {{33055887,-4431773,-521787,6654165,951411, + -6266464,-5158124,6995613,-5397442,-6985227,}, + {4014062,6967095,-11977872,3960002,8001989, + 5130302,-2154812,-1899602,-31954493,-16173976,}, + {16271757,-9212948,23792794,731486,-25808309, + -3546396,6964344,-4767590,10976593,10050757,},}, + {{2533007,-4288439,-24467768,-12387405,-13450051, + 14542280,12876301,13893535,15067764,8594792,}, + {20073501,-11623621,3165391,-13119866,13188608, + -11540496,-10751437,-13482671,29588810,2197295,}, + {-1084082,11831693,6031797,14062724,14748428, + -8159962,-20721760,11742548,31368706,13161200,},}, + {{2050412,-6457589,15321215,5273360,25484180, + 124590,-18187548,-7097255,-6691621,-14604792,}, + {9938196,2162889,-6158074,-1711248,4278932, + -2598531,-22865792,-7168500,-24323168,11746309,}, + {-22691768,-14268164,5965485,9383325,20443693, + 5854192,28250679,-1381811,-10837134,13717818,},}, + {{-8495530,16382250,9548884,-4971523,-4491811, + -3902147,6182256,-12832479,26628081,10395408,}, + {27329048,-15853735,7715764,8717446,-9215518, + -14633480,28982250,-5668414,4227628,242148,}, + {-13279943,-7986904,-7100016,8764468,-27276630, + 3096719,29678419,-9141299,3906709,11265498,},}, + {{11918285,15686328,-17757323,-11217300,-27548967, + 4853165,-27168827,6807359,6871949,-1075745,}, + {-29002610,13984323,-27111812,-2713442,28107359, + -13266203,6155126,15104658,3538727,-7513788,}, + {14103158,11233913,-33165269,9279850,31014152, + 4335090,-1827936,4590951,13960841,12787712,},}, + {{1469134,-16738009,33411928,13942824,8092558, + -8778224,-11165065,1437842,22521552,-2792954,}, + {31352705,-4807352,-25327300,3962447,12541566, + -9399651,-27425693,7964818,-23829869,5541287,}, + {-25732021,-6864887,23848984,3039395,-9147354, + 6022816,-27421653,10590137,25309915,-1584678,},}, + {{-22951376,5048948,31139401,-190316,-19542447, + -626310,-17486305,-16511925,-18851313,-12985140,}, + {-9684890,14681754,30487568,7717771,-10829709, + 9630497,30290549,-10531496,-27798994,-13812825,}, + {5827835,16097107,-24501327,12094619,7413972, + 11447087,28057551,-1793987,-14056981,4359312,},}, + {{26323183,2342588,-21887793,-1623758,-6062284, + 2107090,-28724907,9036464,-19618351,-13055189,}, + {-29697200,14829398,-4596333,14220089,-30022969, + 2955645,12094100,-13693652,-5941445,7047569,}, + {-3201977,14413268,-12058324,-16417589,-9035655, + -7224648,9258160,1399236,30397584,-5684634,},}, +}; + +static void lookup_add(ge *p, ge_precomp *tmp_c, fe tmp_a, fe tmp_b, + const ge_precomp comb[8], const u8 scalar[32], int i) +{ + u8 teeth = (u8)((scalar_bit(scalar, i) ) + + (scalar_bit(scalar, i + 32) << 1) + + (scalar_bit(scalar, i + 64) << 2) + + (scalar_bit(scalar, i + 96) << 3)); + u8 high = teeth >> 3; + u8 index = (teeth ^ (high - 1)) & 7; + FOR (j, 0, 8) { + i32 select = 1 & (((j ^ index) - 1) >> 8); + fe_ccopy(tmp_c->Yp, comb[j].Yp, select); + fe_ccopy(tmp_c->Ym, comb[j].Ym, select); + fe_ccopy(tmp_c->T2, comb[j].T2, select); + } + fe_neg(tmp_a, tmp_c->T2); + fe_cswap(tmp_c->T2, tmp_a , high ^ 1); + fe_cswap(tmp_c->Yp, tmp_c->Ym, high ^ 1); + ge_madd(p, p, tmp_c, tmp_a, tmp_b); +} + +// p = [scalar]B, where B is the base point +static void ge_scalarmult_base(ge *p, const u8 scalar[32]) +{ + // twin 4-bits signed combs, from Mike Hamburg's + // Fast and compact elliptic-curve cryptography (2012) + // 1 / 2 modulo L + static const u8 half_mod_L[32] = { + 247,233,122,46,141,49,9,44,107,206,123,81,239,124,111,10, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8, + }; + // (2^256 - 1) / 2 modulo L + static const u8 half_ones[32] = { + 142,74,204,70,186,24,118,107,184,231,190,57,250,173,119,99, + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,7, + }; + + // All bits set form: 1 means 1, 0 means -1 + u8 s_scalar[32]; + crypto_eddsa_mul_add(s_scalar, scalar, half_mod_L, half_ones); + + // Double and add ladder + fe tmp_a, tmp_b; // temporaries for addition + ge_precomp tmp_c; // temporary for comb lookup + ge tmp_d; // temporary for doubling + fe_1(tmp_c.Yp); + fe_1(tmp_c.Ym); + fe_0(tmp_c.T2); + + // Save a double on the first iteration + ge_zero(p); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_low , s_scalar, 31); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_high, s_scalar, 31+128); + // Regular double & add for the rest + for (int i = 30; i >= 0; i--) { + ge_double(p, p, &tmp_d); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_low , s_scalar, i); + lookup_add(p, &tmp_c, tmp_a, tmp_b, b_comb_high, s_scalar, i+128); + } + // Note: we could save one addition at the end if we assumed the + // scalar fit in 252 bits. Which it does in practice if it is + // selected at random. However, non-random, non-hashed scalars + // *can* overflow 252 bits in practice. Better account for that + // than leaving that kind of subtle corner case. + + WIPE_BUFFER(tmp_a); WIPE_CTX(&tmp_d); + WIPE_BUFFER(tmp_b); WIPE_CTX(&tmp_c); + WIPE_BUFFER(s_scalar); +} + +void crypto_eddsa_scalarbase(u8 point[32], const u8 scalar[32]) +{ + ge P; + ge_scalarmult_base(&P, scalar); + ge_tobytes(point, &P); + WIPE_CTX(&P); +} + +void crypto_eddsa_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32]) +{ + // To allow overlaps, observable writes happen in this order: + // 1. seed + // 2. secret_key + // 3. public_key + u8 a[64]; + COPY(a, seed, 32); + crypto_wipe(seed, 32); + COPY(secret_key, a, 32); + crypto_blake2b(a, 64, a, 32); + crypto_eddsa_trim_scalar(a, a); + crypto_eddsa_scalarbase(secret_key + 32, a); + COPY(public_key, secret_key + 32, 32); + WIPE_BUFFER(a); +} + +static void hash_reduce(u8 h[32], + const u8 *a, size_t a_size, + const u8 *b, size_t b_size, + const u8 *c, size_t c_size) +{ + u8 hash[64]; + crypto_blake2b_ctx ctx; + crypto_blake2b_init (&ctx, 64); + crypto_blake2b_update(&ctx, a, a_size); + crypto_blake2b_update(&ctx, b, b_size); + crypto_blake2b_update(&ctx, c, c_size); + crypto_blake2b_final (&ctx, hash); + crypto_eddsa_reduce(h, hash); +} + +// Digital signature of a message with from a secret key. +// +// The secret key comprises two parts: +// - The seed that generates the key (secret_key[ 0..31]) +// - The public key (secret_key[32..63]) +// +// The seed and the public key are bundled together to make sure users +// don't use mismatched seeds and public keys, which would instantly +// leak the secret scalar and allow forgeries (allowing this to happen +// has resulted in critical vulnerabilities in the wild). +// +// The seed is hashed to derive the secret scalar and a secret prefix. +// The sole purpose of the prefix is to generate a secret random nonce. +// The properties of that nonce must be as follows: +// - Unique: we need a different one for each message. +// - Secret: third parties must not be able to predict it. +// - Random: any detectable bias would break all security. +// +// There are two ways to achieve these properties. The obvious one is +// to simply generate a random number. Here that would be a parameter +// (Monocypher doesn't have an RNG). It works, but then users may reuse +// the nonce by accident, which _also_ leaks the secret scalar and +// allows forgeries. This has happened in the wild too. +// +// This is no good, so instead we generate that nonce deterministically +// by reducing modulo L a hash of the secret prefix and the message. +// The secret prefix makes the nonce unpredictable, the message makes it +// unique, and the hash/reduce removes all bias. +// +// The cost of that safety is hashing the message twice. If that cost +// is unacceptable, there are two alternatives: +// +// - Signing a hash of the message instead of the message itself. This +// is fine as long as the hash is collision resistant. It is not +// compatible with existing "pure" signatures, but at least it's safe. +// +// - Using a random nonce. Please exercise **EXTREME CAUTION** if you +// ever do that. It is absolutely **critical** that the nonce is +// really an unbiased random number between 0 and L-1, never reused, +// and wiped immediately. +// +// To lower the likelihood of complete catastrophe if the RNG is +// either flawed or misused, you can hash the RNG output together with +// the secret prefix and the beginning of the message, and use the +// reduction of that hash instead of the RNG output itself. It's not +// foolproof (you'd need to hash the whole message) but it helps. +// +// Signing a message involves the following operations: +// +// scalar, prefix = HASH(secret_key) +// r = HASH(prefix || message) % L +// R = [r]B +// h = HASH(R || public_key || message) % L +// S = ((h * a) + r) % L +// signature = R || S +void crypto_eddsa_sign(u8 signature [64], const u8 secret_key[64], + const u8 *message, size_t message_size) +{ + u8 a[64]; // secret scalar and prefix + u8 r[32]; // secret deterministic "random" nonce + u8 h[32]; // publically verifiable hash of the message (not wiped) + u8 R[32]; // first half of the signature (allows overlapping inputs) + + crypto_blake2b(a, 64, secret_key, 32); + crypto_eddsa_trim_scalar(a, a); + hash_reduce(r, a + 32, 32, message, message_size, 0, 0); + crypto_eddsa_scalarbase(R, r); + hash_reduce(h, R, 32, secret_key + 32, 32, message, message_size); + COPY(signature, R, 32); + crypto_eddsa_mul_add(signature + 32, h, a, r); + + WIPE_BUFFER(a); + WIPE_BUFFER(r); +} + +// To check the signature R, S of the message M with the public key A, +// there are 3 steps: +// +// compute h = HASH(R || A || message) % L +// check that A is on the curve. +// check that R == [s]B - [h]A +// +// The last two steps are done in crypto_eddsa_check_equation() +int crypto_eddsa_check(const u8 signature[64], const u8 public_key[32], + const u8 *message, size_t message_size) +{ + u8 h[32]; + hash_reduce(h, signature, 32, public_key, 32, message, message_size); + return crypto_eddsa_check_equation(signature, public_key, h); +} + +///////////////////////// +/// EdDSA <--> X25519 /// +///////////////////////// +void crypto_eddsa_to_x25519(u8 x25519[32], const u8 eddsa[32]) +{ + // (u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x) + // Only converting y to u, the sign of x is ignored. + fe t1, t2; + fe_frombytes(t2, eddsa); + fe_add(t1, fe_one, t2); + fe_sub(t2, fe_one, t2); + fe_invert(t2, t2); + fe_mul(t1, t1, t2); + fe_tobytes(x25519, t1); + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); +} + +void crypto_x25519_to_eddsa(u8 eddsa[32], const u8 x25519[32]) +{ + // (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1)) + // Only converting u to y, x is assumed positive. + fe t1, t2; + fe_frombytes(t2, x25519); + fe_sub(t1, t2, fe_one); + fe_add(t2, t2, fe_one); + fe_invert(t2, t2); + fe_mul(t1, t1, t2); + fe_tobytes(eddsa, t1); + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); +} + +///////////////////////////////////////////// +/// Dirty ephemeral public key generation /// +///////////////////////////////////////////// + +// Those functions generates a public key, *without* clearing the +// cofactor. Sending that key over the network leaks 3 bits of the +// private key. Use only to generate ephemeral keys that will be hidden +// with crypto_curve_to_hidden(). +// +// The public key is otherwise compatible with crypto_x25519(), which +// properly clears the cofactor. +// +// Note that the distribution of the resulting public keys is almost +// uniform. Flipping the sign of the v coordinate (not provided by this +// function), covers the entire key space almost perfectly, where +// "almost" means a 2^-128 bias (undetectable). This uniformity is +// needed to ensure the proper randomness of the resulting +// representatives (once we apply crypto_curve_to_hidden()). +// +// Recall that Curve25519 has order C = 2^255 + e, with e < 2^128 (not +// to be confused with the prime order of the main subgroup, L, which is +// 8 times less than that). +// +// Generating all points would require us to multiply a point of order C +// (the base point plus any point of order 8) by all scalars from 0 to +// C-1. Clamping limits us to scalars between 2^254 and 2^255 - 1. But +// by negating the resulting point at random, we also cover scalars from +// -2^255 + 1 to -2^254 (which modulo C is congruent to e+1 to 2^254 + e). +// +// In practice: +// - Scalars from 0 to e + 1 are never generated +// - Scalars from 2^255 to 2^255 + e are never generated +// - Scalars from 2^254 + 1 to 2^254 + e are generated twice +// +// Since e < 2^128, detecting this bias requires observing over 2^100 +// representatives from a given source (this will never happen), *and* +// recovering enough of the private key to determine that they do, or do +// not, belong to the biased set (this practically requires solving +// discrete logarithm, which is conjecturally intractable). +// +// In practice, this means the bias is impossible to detect. + +// s + (x*L) % 8*L +// Guaranteed to fit in 256 bits iff s fits in 255 bits. +// L < 2^253 +// x%8 < 2^3 +// L * (x%8) < 2^255 +// s < 2^255 +// s + L * (x%8) < 2^256 +static void add_xl(u8 s[32], u8 x) +{ + u64 mod8 = x & 7; + u64 carry = 0; + FOR (i , 0, 8) { + carry = carry + load32_le(s + 4*i) + L[i] * mod8; + store32_le(s + 4*i, (u32)carry); + carry >>= 32; + } +} + +// "Small" dirty ephemeral key. +// Use if you need to shrink the size of the binary, and can afford to +// slow down by a factor of two (compared to the fast version) +// +// This version works by decoupling the cofactor from the main factor. +// +// - The trimmed scalar determines the main factor +// - The clamped bits of the scalar determine the cofactor. +// +// Cofactor and main factor are combined into a single scalar, which is +// then multiplied by a point of order 8*L (unlike the base point, which +// has prime order). That "dirty" base point is the addition of the +// regular base point (9), and a point of order 8. +void crypto_x25519_dirty_small(u8 public_key[32], const u8 secret_key[32]) +{ + // Base point of order 8*L + // Raw scalar multiplication with it does not clear the cofactor, + // and the resulting public key will reveal 3 bits of the scalar. + // + // The low order component of this base point has been chosen + // to yield the same results as crypto_x25519_dirty_fast(). + static const u8 dirty_base_point[32] = { + 0xd8, 0x86, 0x1a, 0xa2, 0x78, 0x7a, 0xd9, 0x26, + 0x8b, 0x74, 0x74, 0xb6, 0x82, 0xe3, 0xbe, 0xc3, + 0xce, 0x36, 0x9a, 0x1e, 0x5e, 0x31, 0x47, 0xa2, + 0x6d, 0x37, 0x7c, 0xfd, 0x20, 0xb5, 0xdf, 0x75, + }; + // separate the main factor & the cofactor of the scalar + u8 scalar[32]; + crypto_eddsa_trim_scalar(scalar, secret_key); + + // Separate the main factor and the cofactor + // + // The scalar is trimmed, so its cofactor is cleared. The three + // least significant bits however still have a main factor. We must + // remove it for X25519 compatibility. + // + // cofactor = lsb * L (modulo 8*L) + // combined = scalar + cofactor (modulo 8*L) + add_xl(scalar, secret_key[0]); + scalarmult(public_key, scalar, dirty_base_point, 256); + WIPE_BUFFER(scalar); +} + +// Select low order point +// We're computing the [cofactor]lop scalar multiplication, where: +// +// cofactor = tweak & 7. +// lop = (lop_x, lop_y) +// lop_x = sqrt((sqrt(d + 1) + 1) / d) +// lop_y = -lop_x * sqrtm1 +// +// The low order point has order 8. There are 4 such points. We've +// chosen the one whose both coordinates are positive (below p/2). +// The 8 low order points are as follows: +// +// [0]lop = ( 0 , 1 ) +// [1]lop = ( lop_x , lop_y) +// [2]lop = ( sqrt(-1), -0 ) +// [3]lop = ( lop_x , -lop_y) +// [4]lop = (-0 , -1 ) +// [5]lop = (-lop_x , -lop_y) +// [6]lop = (-sqrt(-1), 0 ) +// [7]lop = (-lop_x , lop_y) +// +// The x coordinate is either 0, sqrt(-1), lop_x, or their opposite. +// The y coordinate is either 0, -1 , lop_y, or their opposite. +// The pattern for both is the same, except for a rotation of 2 (modulo 8) +// +// This helper function captures the pattern, and we can use it thus: +// +// select_lop(x, lop_x, sqrtm1, cofactor); +// select_lop(y, lop_y, fe_one, cofactor + 2); +// +// This is faster than an actual scalar multiplication, +// and requires less code than naive constant time look up. +static void select_lop(fe out, const fe x, const fe k, u8 cofactor) +{ + fe tmp; + fe_0(out); + fe_ccopy(out, k , (cofactor >> 1) & 1); // bit 1 + fe_ccopy(out, x , (cofactor >> 0) & 1); // bit 0 + fe_neg (tmp, out); + fe_ccopy(out, tmp, (cofactor >> 2) & 1); // bit 2 + WIPE_BUFFER(tmp); +} + +// "Fast" dirty ephemeral key +// We use this one by default. +// +// This version works by performing a regular scalar multiplication, +// then add a low order point. The scalar multiplication is done in +// Edwards space for more speed (*2 compared to the "small" version). +// The cost is a bigger binary for programs that don't also sign messages. +void crypto_x25519_dirty_fast(u8 public_key[32], const u8 secret_key[32]) +{ + // Compute clean scalar multiplication + u8 scalar[32]; + ge pk; + crypto_eddsa_trim_scalar(scalar, secret_key); + ge_scalarmult_base(&pk, scalar); + + // Compute low order point + fe t1, t2; + select_lop(t1, lop_x, sqrtm1, secret_key[0]); + select_lop(t2, lop_y, fe_one, secret_key[0] + 2); + ge_precomp low_order_point; + fe_add(low_order_point.Yp, t2, t1); + fe_sub(low_order_point.Ym, t2, t1); + fe_mul(low_order_point.T2, t2, t1); + fe_mul(low_order_point.T2, low_order_point.T2, D2); + + // Add low order point to the public key + ge_madd(&pk, &pk, &low_order_point, t1, t2); + + // Convert to Montgomery u coordinate (we ignore the sign) + fe_add(t1, pk.Z, pk.Y); + fe_sub(t2, pk.Z, pk.Y); + fe_invert(t2, t2); + fe_mul(t1, t1, t2); + + fe_tobytes(public_key, t1); + + WIPE_BUFFER(t1); WIPE_CTX(&pk); + WIPE_BUFFER(t2); WIPE_CTX(&low_order_point); + WIPE_BUFFER(scalar); +} + +/////////////////// +/// Elligator 2 /// +/////////////////// +static const fe A = {486662}; + +// Elligator direct map +// +// Computes the point corresponding to a representative, encoded in 32 +// bytes (little Endian). Since positive representatives fits in 254 +// bits, The two most significant bits are ignored. +// +// From the paper: +// w = -A / (fe(1) + non_square * r^2) +// e = chi(w^3 + A*w^2 + w) +// u = e*w - (fe(1)-e)*(A//2) +// v = -e * sqrt(u^3 + A*u^2 + u) +// +// We ignore v because we don't need it for X25519 (the Montgomery +// ladder only uses u). +// +// Note that e is either 0, 1 or -1 +// if e = 0 u = 0 and v = 0 +// if e = 1 u = w +// if e = -1 u = -w - A = w * non_square * r^2 +// +// Let r1 = non_square * r^2 +// Let r2 = 1 + r1 +// Note that r2 cannot be zero, -1/non_square is not a square. +// We can (tediously) verify that: +// w^3 + A*w^2 + w = (A^2*r1 - r2^2) * A / r2^3 +// Therefore: +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) * 1 +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3)) * chi(r2^6) +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * (A / r2^3) * r2^6) +// chi(w^3 + A*w^2 + w) = chi((A^2*r1 - r2^2) * A * r2^3) +// Corollary: +// e = 1 if (A^2*r1 - r2^2) * A * r2^3) is a non-zero square +// e = -1 if (A^2*r1 - r2^2) * A * r2^3) is not a square +// Note that w^3 + A*w^2 + w (and therefore e) can never be zero: +// w^3 + A*w^2 + w = w * (w^2 + A*w + 1) +// w^3 + A*w^2 + w = w * (w^2 + A*w + A^2/4 - A^2/4 + 1) +// w^3 + A*w^2 + w = w * (w + A/2)^2 - A^2/4 + 1) +// which is zero only if: +// w = 0 (impossible) +// (w + A/2)^2 = A^2/4 - 1 (impossible, because A^2/4-1 is not a square) +// +// Let isr = invsqrt((A^2*r1 - r2^2) * A * r2^3) +// isr = sqrt(1 / ((A^2*r1 - r2^2) * A * r2^3)) if e = 1 +// isr = sqrt(sqrt(-1) / ((A^2*r1 - r2^2) * A * r2^3)) if e = -1 +// +// if e = 1 +// let u1 = -A * (A^2*r1 - r2^2) * A * r2^2 * isr^2 +// u1 = w +// u1 = u +// +// if e = -1 +// let ufactor = -non_square * sqrt(-1) * r^2 +// let vfactor = sqrt(ufactor) +// let u2 = -A * (A^2*r1 - r2^2) * A * r2^2 * isr^2 * ufactor +// u2 = w * -1 * -non_square * r^2 +// u2 = w * non_square * r^2 +// u2 = u +void crypto_elligator_map(u8 curve[32], const u8 hidden[32]) +{ + fe r, u, t1, t2, t3; + fe_frombytes_mask(r, hidden, 2); // r is encoded in 254 bits. + fe_sq(r, r); + fe_add(t1, r, r); + fe_add(u, t1, fe_one); + fe_sq (t2, u); + fe_mul(t3, A2, t1); + fe_sub(t3, t3, t2); + fe_mul(t3, t3, A); + fe_mul(t1, t2, u); + fe_mul(t1, t3, t1); + int is_square = invsqrt(t1, t1); + fe_mul(u, r, ufactor); + fe_ccopy(u, fe_one, is_square); + fe_sq (t1, t1); + fe_mul(u, u, A); + fe_mul(u, u, t3); + fe_mul(u, u, t2); + fe_mul(u, u, t1); + fe_neg(u, u); + fe_tobytes(curve, u); + + WIPE_BUFFER(t1); WIPE_BUFFER(r); + WIPE_BUFFER(t2); WIPE_BUFFER(u); + WIPE_BUFFER(t3); +} + +// Elligator inverse map +// +// Computes the representative of a point, if possible. If not, it does +// nothing and returns -1. Note that the success of the operation +// depends only on the point (more precisely its u coordinate). The +// tweak parameter is used only upon success +// +// The tweak should be a random byte. Beyond that, its contents are an +// implementation detail. Currently, the tweak comprises: +// - Bit 1 : sign of the v coordinate (0 if positive, 1 if negative) +// - Bit 2-5: not used +// - Bits 6-7: random padding +// +// From the paper: +// Let sq = -non_square * u * (u+A) +// if sq is not a square, or u = -A, there is no mapping +// Assuming there is a mapping: +// if v is positive: r = sqrt(-u / (non_square * (u+A))) +// if v is negative: r = sqrt(-(u+A) / (non_square * u )) +// +// We compute isr = invsqrt(-non_square * u * (u+A)) +// if it wasn't a square, abort. +// else, isr = sqrt(-1 / (non_square * u * (u+A)) +// +// If v is positive, we return isr * u: +// isr * u = sqrt(-1 / (non_square * u * (u+A)) * u +// isr * u = sqrt(-u / (non_square * (u+A)) +// +// If v is negative, we return isr * (u+A): +// isr * (u+A) = sqrt(-1 / (non_square * u * (u+A)) * (u+A) +// isr * (u+A) = sqrt(-(u+A) / (non_square * u) +int crypto_elligator_rev(u8 hidden[32], const u8 public_key[32], u8 tweak) +{ + fe t1, t2, t3; + fe_frombytes(t1, public_key); // t1 = u + + fe_add(t2, t1, A); // t2 = u + A + fe_mul(t3, t1, t2); + fe_mul_small(t3, t3, -2); + int is_square = invsqrt(t3, t3); // t3 = sqrt(-1 / non_square * u * (u+A)) + if (is_square) { + // The only variable time bit. This ultimately reveals how many + // tries it took us to find a representable key. + // This does not affect security as long as we try keys at random. + + fe_ccopy (t1, t2, tweak & 1); // multiply by u if v is positive, + fe_mul (t3, t1, t3); // multiply by u+A otherwise + fe_mul_small(t1, t3, 2); + fe_neg (t2, t3); + fe_ccopy (t3, t2, fe_isodd(t1)); + fe_tobytes(hidden, t3); + + // Pad with two random bits + hidden[31] |= tweak & 0xc0; + } + + WIPE_BUFFER(t1); + WIPE_BUFFER(t2); + WIPE_BUFFER(t3); + return is_square - 1; +} + +void crypto_elligator_key_pair(u8 hidden[32], u8 secret_key[32], u8 seed[32]) +{ + u8 pk [32]; // public key + u8 buf[64]; // seed + representative + COPY(buf + 32, seed, 32); + do { + crypto_chacha20_djb(buf, 0, 64, buf+32, zero, 0); + crypto_x25519_dirty_fast(pk, buf); // or the "small" version + } while(crypto_elligator_rev(buf+32, pk, buf[32])); + // Note that the return value of crypto_elligator_rev() is + // independent from its tweak parameter. + // Therefore, buf[32] is not actually reused. Either we loop one + // more time and buf[32] is used for the new seed, or we succeeded, + // and buf[32] becomes the tweak parameter. + + crypto_wipe(seed, 32); + COPY(hidden , buf + 32, 32); + COPY(secret_key, buf , 32); + WIPE_BUFFER(buf); + WIPE_BUFFER(pk); +} + +/////////////////////// +/// Scalar division /// +/////////////////////// + +// Montgomery reduction. +// Divides x by (2^256), and reduces the result modulo L +// +// Precondition: +// x < L * 2^256 +// Constants: +// r = 2^256 (makes division by r trivial) +// k = (r * (1/r) - 1) // L (1/r is computed modulo L ) +// Algorithm: +// s = (x * k) % r +// t = x + s*L (t is always a multiple of r) +// u = (t/r) % L (u is always below 2*L, conditional subtraction is enough) +static void redc(u32 u[8], u32 x[16]) +{ + static const u32 k[8] = { + 0x12547e1b, 0xd2b51da3, 0xfdba84ff, 0xb1a206f2, + 0xffa36bea, 0x14e75438, 0x6fe91836, 0x9db6c6f2, + }; + + // s = x * k (modulo 2^256) + // This is cheaper than the full multiplication. + u32 s[8] = {0}; + FOR (i, 0, 8) { + u64 carry = 0; + FOR (j, 0, 8-i) { + carry += s[i+j] + (u64)x[i] * k[j]; + s[i+j] = (u32)carry; + carry >>= 32; + } + } + u32 t[16] = {0}; + multiply(t, s, L); + + // t = t + x + u64 carry = 0; + FOR (i, 0, 16) { + carry += (u64)t[i] + x[i]; + t[i] = (u32)carry; + carry >>= 32; + } + + // u = (t / 2^256) % L + // Note that t / 2^256 is always below 2*L, + // So a constant time conditional subtraction is enough + remove_l(u, t+8); + + WIPE_BUFFER(s); + WIPE_BUFFER(t); +} + +void crypto_x25519_inverse(u8 blind_salt [32], const u8 private_key[32], + const u8 curve_point[32]) +{ + static const u8 Lm2[32] = { // L - 2 + 0xeb, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, + 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + }; + // 1 in Montgomery form + u32 m_inv [8] = { + 0x8d98951d, 0xd6ec3174, 0x737dcf70, 0xc6ef5bf4, + 0xfffffffe, 0xffffffff, 0xffffffff, 0x0fffffff, + }; + + u8 scalar[32]; + crypto_eddsa_trim_scalar(scalar, private_key); + + // Convert the scalar in Montgomery form + // m_scl = scalar * 2^256 (modulo L) + u32 m_scl[8]; + { + u32 tmp[16]; + ZERO(tmp, 8); + load32_le_buf(tmp+8, scalar, 8); + mod_l(scalar, tmp); + load32_le_buf(m_scl, scalar, 8); + WIPE_BUFFER(tmp); // Wipe ASAP to save stack space + } + + // Compute the inverse + u32 product[16]; + for (int i = 252; i >= 0; i--) { + ZERO(product, 16); + multiply(product, m_inv, m_inv); + redc(m_inv, product); + if (scalar_bit(Lm2, i)) { + ZERO(product, 16); + multiply(product, m_inv, m_scl); + redc(m_inv, product); + } + } + // Convert the inverse *out* of Montgomery form + // scalar = m_inv / 2^256 (modulo L) + COPY(product, m_inv, 8); + ZERO(product + 8, 8); + redc(m_inv, product); + store32_le_buf(scalar, m_inv, 8); // the *inverse* of the scalar + + // Clear the cofactor of scalar: + // cleared = scalar * (3*L + 1) (modulo 8*L) + // cleared = scalar + scalar * 3 * L (modulo 8*L) + // Note that (scalar * 3) is reduced modulo 8, so we only need the + // first byte. + add_xl(scalar, scalar[0] * 3); + + // Recall that 8*L < 2^256. However it is also very close to + // 2^255. If we spanned the ladder over 255 bits, random tests + // wouldn't catch the off-by-one error. + scalarmult(blind_salt, scalar, curve_point, 256); + + WIPE_BUFFER(scalar); WIPE_BUFFER(m_scl); + WIPE_BUFFER(product); WIPE_BUFFER(m_inv); +} + +//////////////////////////////// +/// Authenticated encryption /// +//////////////////////////////// +static void lock_auth(u8 mac[16], const u8 auth_key[32], + const u8 *ad , size_t ad_size, + const u8 *cipher_text, size_t text_size) +{ + u8 sizes[16]; // Not secret, not wiped + store64_le(sizes + 0, ad_size); + store64_le(sizes + 8, text_size); + crypto_poly1305_ctx poly_ctx; // auto wiped... + crypto_poly1305_init (&poly_ctx, auth_key); + crypto_poly1305_update(&poly_ctx, ad , ad_size); + crypto_poly1305_update(&poly_ctx, zero , gap(ad_size, 16)); + crypto_poly1305_update(&poly_ctx, cipher_text, text_size); + crypto_poly1305_update(&poly_ctx, zero , gap(text_size, 16)); + crypto_poly1305_update(&poly_ctx, sizes , 16); + crypto_poly1305_final (&poly_ctx, mac); // ...here +} + +void crypto_aead_init_x(crypto_aead_ctx *ctx, + u8 const key[32], const u8 nonce[24]) +{ + crypto_chacha20_h(ctx->key, key, nonce); + COPY(ctx->nonce, nonce + 16, 8); + ctx->counter = 0; +} + +void crypto_aead_init_djb(crypto_aead_ctx *ctx, + const u8 key[32], const u8 nonce[8]) +{ + COPY(ctx->key , key , 32); + COPY(ctx->nonce, nonce, 8); + ctx->counter = 0; +} + +void crypto_aead_init_ietf(crypto_aead_ctx *ctx, + const u8 key[32], const u8 nonce[12]) +{ + COPY(ctx->key , key , 32); + COPY(ctx->nonce, nonce + 4, 8); + ctx->counter = (u64)load32_le(nonce) << 32; +} + +void crypto_aead_write(crypto_aead_ctx *ctx, u8 *cipher_text, u8 mac[16], + const u8 *ad, size_t ad_size, + const u8 *plain_text, size_t text_size) +{ + u8 auth_key[64]; // the last 32 bytes are used for rekeying. + crypto_chacha20_djb(auth_key, 0, 64, ctx->key, ctx->nonce, ctx->counter); + crypto_chacha20_djb(cipher_text, plain_text, text_size, + ctx->key, ctx->nonce, ctx->counter + 1); + lock_auth(mac, auth_key, ad, ad_size, cipher_text, text_size); + COPY(ctx->key, auth_key + 32, 32); + WIPE_BUFFER(auth_key); +} + +int crypto_aead_read(crypto_aead_ctx *ctx, u8 *plain_text, const u8 mac[16], + const u8 *ad, size_t ad_size, + const u8 *cipher_text, size_t text_size) +{ + u8 auth_key[64]; // the last 32 bytes are used for rekeying. + u8 real_mac[16]; + crypto_chacha20_djb(auth_key, 0, 64, ctx->key, ctx->nonce, ctx->counter); + lock_auth(real_mac, auth_key, ad, ad_size, cipher_text, text_size); + int mismatch = crypto_verify16(mac, real_mac); + if (!mismatch) { + crypto_chacha20_djb(plain_text, cipher_text, text_size, + ctx->key, ctx->nonce, ctx->counter + 1); + COPY(ctx->key, auth_key + 32, 32); + } + WIPE_BUFFER(auth_key); + WIPE_BUFFER(real_mac); + return mismatch; +} + +void crypto_aead_lock(u8 *cipher_text, u8 mac[16], const u8 key[32], + const u8 nonce[24], const u8 *ad, size_t ad_size, + const u8 *plain_text, size_t text_size) +{ + crypto_aead_ctx ctx; + crypto_aead_init_x(&ctx, key, nonce); + crypto_aead_write(&ctx, cipher_text, mac, ad, ad_size, + plain_text, text_size); + crypto_wipe(&ctx, sizeof(ctx)); +} + +int crypto_aead_unlock(u8 *plain_text, const u8 mac[16], const u8 key[32], + const u8 nonce[24], const u8 *ad, size_t ad_size, + const u8 *cipher_text, size_t text_size) +{ + crypto_aead_ctx ctx; + crypto_aead_init_x(&ctx, key, nonce); + int mismatch = crypto_aead_read(&ctx, plain_text, mac, ad, ad_size, + cipher_text, text_size); + crypto_wipe(&ctx, sizeof(ctx)); + return mismatch; +} + +#ifdef MONOCYPHER_CPP_NAMESPACE +} +#endif diff --git a/third_party/monocypher/monocypher.h b/third_party/monocypher/monocypher.h new file mode 100644 index 0000000..cf635e8 --- /dev/null +++ b/third_party/monocypher/monocypher.h @@ -0,0 +1,321 @@ +// Monocypher version __git__ +// +// This file is dual-licensed. Choose whichever licence you want from +// the two licences listed below. +// +// The first licence is a regular 2-clause BSD licence. The second licence +// is the CC-0 from Creative Commons. It is intended to release Monocypher +// to the public domain. The BSD licence serves as a fallback option. +// +// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0 +// +// ------------------------------------------------------------------------ +// +// Copyright (c) 2017-2019, Loup Vaillant +// All rights reserved. +// +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// ------------------------------------------------------------------------ +// +// Written in 2017-2019 by Loup Vaillant +// +// To the extent possible under law, the author(s) have dedicated all copyright +// and related neighboring rights to this software to the public domain +// worldwide. This software is distributed without any warranty. +// +// You should have received a copy of the CC0 Public Domain Dedication along +// with this software. If not, see +// + +#ifndef MONOCYPHER_H +#define MONOCYPHER_H + +#include +#include + +#ifdef MONOCYPHER_CPP_NAMESPACE +namespace MONOCYPHER_CPP_NAMESPACE { +#elif defined(__cplusplus) +extern "C" { +#endif + +// Constant time comparisons +// ------------------------- + +// Return 0 if a and b are equal, -1 otherwise +int crypto_verify16(const uint8_t a[16], const uint8_t b[16]); +int crypto_verify32(const uint8_t a[32], const uint8_t b[32]); +int crypto_verify64(const uint8_t a[64], const uint8_t b[64]); + + +// Erase sensitive data +// -------------------- +void crypto_wipe(void *secret, size_t size); + + +// Authenticated encryption +// ------------------------ +void crypto_aead_lock(uint8_t *cipher_text, + uint8_t mac [16], + const uint8_t key [32], + const uint8_t nonce[24], + const uint8_t *ad, size_t ad_size, + const uint8_t *plain_text, size_t text_size); +int crypto_aead_unlock(uint8_t *plain_text, + const uint8_t mac [16], + const uint8_t key [32], + const uint8_t nonce[24], + const uint8_t *ad, size_t ad_size, + const uint8_t *cipher_text, size_t text_size); + +// Authenticated stream +// -------------------- +typedef struct { + uint64_t counter; + uint8_t key[32]; + uint8_t nonce[8]; +} crypto_aead_ctx; + +void crypto_aead_init_x(crypto_aead_ctx *ctx, + const uint8_t key[32], const uint8_t nonce[24]); +void crypto_aead_init_djb(crypto_aead_ctx *ctx, + const uint8_t key[32], const uint8_t nonce[8]); +void crypto_aead_init_ietf(crypto_aead_ctx *ctx, + const uint8_t key[32], const uint8_t nonce[12]); + +void crypto_aead_write(crypto_aead_ctx *ctx, + uint8_t *cipher_text, + uint8_t mac[16], + const uint8_t *ad , size_t ad_size, + const uint8_t *plain_text, size_t text_size); +int crypto_aead_read(crypto_aead_ctx *ctx, + uint8_t *plain_text, + const uint8_t mac[16], + const uint8_t *ad , size_t ad_size, + const uint8_t *cipher_text, size_t text_size); + + +// General purpose hash (BLAKE2b) +// ------------------------------ + +// Direct interface +void crypto_blake2b(uint8_t *hash, size_t hash_size, + const uint8_t *message, size_t message_size); + +void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size, + const uint8_t *key, size_t key_size, + const uint8_t *message, size_t message_size); + +// Incremental interface +typedef struct { + // Do not rely on the size or contents of this type, + // for they may change without notice. + uint64_t hash[8]; + uint64_t input_offset[2]; + uint64_t input[16]; + size_t input_idx; + size_t hash_size; +} crypto_blake2b_ctx; + +void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size); +void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size, + const uint8_t *key, size_t key_size); +void crypto_blake2b_update(crypto_blake2b_ctx *ctx, + const uint8_t *message, size_t message_size); +void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash); + + +// Password key derivation (Argon2) +// -------------------------------- +#define CRYPTO_ARGON2_D 0 +#define CRYPTO_ARGON2_I 1 +#define CRYPTO_ARGON2_ID 2 + +typedef struct { + uint32_t algorithm; // Argon2d, Argon2i, Argon2id + uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes + uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i) + uint32_t nb_lanes; // parallelism level (single threaded anyway) +} crypto_argon2_config; + +typedef struct { + const uint8_t *pass; + const uint8_t *salt; + uint32_t pass_size; + uint32_t salt_size; // 16 bytes recommended +} crypto_argon2_inputs; + +typedef struct { + const uint8_t *key; // may be NULL if no key + const uint8_t *ad; // may be NULL if no additional data + uint32_t key_size; // 0 if no key (32 bytes recommended otherwise) + uint32_t ad_size; // 0 if no additional data +} crypto_argon2_extras; + +extern const crypto_argon2_extras crypto_argon2_no_extras; + +void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area, + crypto_argon2_config config, + crypto_argon2_inputs inputs, + crypto_argon2_extras extras); + + +// Key exchange (X-25519) +// ---------------------- + +// Shared secrets are not quite random. +// Hash them to derive an actual shared key. +void crypto_x25519_public_key(uint8_t public_key[32], + const uint8_t secret_key[32]); +void crypto_x25519(uint8_t raw_shared_secret[32], + const uint8_t your_secret_key [32], + const uint8_t their_public_key [32]); + +// Conversion to EdDSA +void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]); + +// scalar "division" +// Used for OPRF. Be aware that exponential blinding is less secure +// than Diffie-Hellman key exchange. +void crypto_x25519_inverse(uint8_t blind_salt [32], + const uint8_t private_key[32], + const uint8_t curve_point[32]); + +// "Dirty" versions of x25519_public_key(). +// Use with crypto_elligator_rev(). +// Leaks 3 bits of the private key. +void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]); +void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]); + + +// Signatures +// ---------- + +// EdDSA with curve25519 + BLAKE2b +void crypto_eddsa_key_pair(uint8_t secret_key[64], + uint8_t public_key[32], + uint8_t seed[32]); +void crypto_eddsa_sign(uint8_t signature [64], + const uint8_t secret_key[64], + const uint8_t *message, size_t message_size); +int crypto_eddsa_check(const uint8_t signature [64], + const uint8_t public_key[32], + const uint8_t *message, size_t message_size); + +// Conversion to X25519 +void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]); + +// EdDSA building blocks +void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]); +void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]); +void crypto_eddsa_mul_add(uint8_t r[32], + const uint8_t a[32], + const uint8_t b[32], + const uint8_t c[32]); +void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]); +int crypto_eddsa_check_equation(const uint8_t signature[64], + const uint8_t public_key[32], + const uint8_t h_ram[32]); + + +// Chacha20 +// -------- + +// Specialised hash. +// Used to hash X25519 shared secrets. +void crypto_chacha20_h(uint8_t out[32], + const uint8_t key[32], + const uint8_t in [16]); + +// Unauthenticated stream cipher. +// Don't forget to add authentication. +uint64_t crypto_chacha20_djb(uint8_t *cipher_text, + const uint8_t *plain_text, + size_t text_size, + const uint8_t key[32], + const uint8_t nonce[8], + uint64_t ctr); +uint32_t crypto_chacha20_ietf(uint8_t *cipher_text, + const uint8_t *plain_text, + size_t text_size, + const uint8_t key[32], + const uint8_t nonce[12], + uint32_t ctr); +uint64_t crypto_chacha20_x(uint8_t *cipher_text, + const uint8_t *plain_text, + size_t text_size, + const uint8_t key[32], + const uint8_t nonce[24], + uint64_t ctr); + + +// Poly 1305 +// --------- + +// This is a *one time* authenticator. +// Disclosing the mac reveals the key. +// See crypto_lock() on how to use it properly. + +// Direct interface +void crypto_poly1305(uint8_t mac[16], + const uint8_t *message, size_t message_size, + const uint8_t key[32]); + +// Incremental interface +typedef struct { + // Do not rely on the size or contents of this type, + // for they may change without notice. + uint8_t c[16]; // chunk of the message + size_t c_idx; // How many bytes are there in the chunk. + uint32_t r [4]; // constant multiplier (from the secret key) + uint32_t pad[4]; // random number added at the end (from the secret key) + uint32_t h [5]; // accumulated hash +} crypto_poly1305_ctx; + +void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]); +void crypto_poly1305_update(crypto_poly1305_ctx *ctx, + const uint8_t *message, size_t message_size); +void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]); + + +// Elligator 2 +// ----------- + +// Elligator mappings proper +void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]); +int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32], + uint8_t tweak); + +// Easy to use key pair generation +void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32], + uint8_t seed[32]); + +#ifdef __cplusplus +} +#endif + +#endif // MONOCYPHER_H diff --git a/third_party/tinyaes/aes.c b/third_party/tinyaes/aes.c new file mode 100644 index 0000000..4481f7b --- /dev/null +++ b/third_party/tinyaes/aes.c @@ -0,0 +1,572 @@ +/* + +This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. +Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. + +The implementation is verified against the test vectors in: + National Institute of Standards and Technology Special Publication 800-38A 2001 ED + +ECB-AES128 +---------- + + plain-text: + 6bc1bee22e409f96e93d7e117393172a + ae2d8a571e03ac9c9eb76fac45af8e51 + 30c81c46a35ce411e5fbc1191a0a52ef + f69f2445df4f9b17ad2b417be66c3710 + + key: + 2b7e151628aed2a6abf7158809cf4f3c + + resulting cipher + 3ad77bb40d7a3660a89ecaf32466ef97 + f5d3d58503b9699de785895a96fdbaaf + 43b1cd7f598ece23881b00e3ed030688 + 7b0c785e27e8ad3f8223207104725dd4 + + +NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0) + You should pad the end of the string with zeros if this is not the case. + For AES192/256 the key size is proportionally larger. + +*/ + + +/*****************************************************************************/ +/* Includes: */ +/*****************************************************************************/ +#include // CBC mode, for memset +#include "aes.h" + +/*****************************************************************************/ +/* Defines: */ +/*****************************************************************************/ +// The number of columns comprising a state in AES. This is a constant in AES. Value=4 +#define Nb 4 + +#if defined(AES256) && (AES256 == 1) + #define Nk 8 + #define Nr 14 +#elif defined(AES192) && (AES192 == 1) + #define Nk 6 + #define Nr 12 +#else + #define Nk 4 // The number of 32 bit words in a key. + #define Nr 10 // The number of rounds in AES Cipher. +#endif + +// jcallan@github points out that declaring Multiply as a function +// reduces code size considerably with the Keil ARM compiler. +// See this link for more information: https://github.com/kokke/tiny-AES-C/pull/3 +#ifndef MULTIPLY_AS_A_FUNCTION + #define MULTIPLY_AS_A_FUNCTION 0 +#endif + + + + +/*****************************************************************************/ +/* Private variables: */ +/*****************************************************************************/ +// state - array holding the intermediate results during decryption. +typedef uint8_t state_t[4][4]; + + + +// The lookup-tables are marked const so they can be placed in read-only storage instead of RAM +// The numbers below can be computed dynamically trading ROM for RAM - +// This can be useful in (embedded) bootloader applications, where ROM is often limited. +static const uint8_t sbox[256] = { + //0 1 2 3 4 5 6 7 8 9 A B C D E F + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; + +#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) +static const uint8_t rsbox[256] = { + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d }; +#endif + +// The round constant word array, Rcon[i], contains the values given by +// x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8) +static const uint8_t Rcon[11] = { + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 }; + +/* + * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES-C/pull/12), + * that you can remove most of the elements in the Rcon array, because they are unused. + * + * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon + * + * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed), + * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm." + */ + + +/*****************************************************************************/ +/* Private functions: */ +/*****************************************************************************/ +/* +static uint8_t getSBoxValue(uint8_t num) +{ + return sbox[num]; +} +*/ +#define getSBoxValue(num) (sbox[(num)]) + +// This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states. +static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key) +{ + unsigned i, j, k; + uint8_t tempa[4]; // Used for the column/row operations + + // The first round key is the key itself. + for (i = 0; i < Nk; ++i) + { + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + } + + // All other round keys are found from the previous round keys. + for (i = Nk; i < Nb * (Nr + 1); ++i) + { + { + k = (i - 1) * 4; + tempa[0]=RoundKey[k + 0]; + tempa[1]=RoundKey[k + 1]; + tempa[2]=RoundKey[k + 2]; + tempa[3]=RoundKey[k + 3]; + + } + + if (i % Nk == 0) + { + // This function shifts the 4 bytes in a word to the left once. + // [a0,a1,a2,a3] becomes [a1,a2,a3,a0] + + // Function RotWord() + { + const uint8_t u8tmp = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = u8tmp; + } + + // SubWord() is a function that takes a four-byte input word and + // applies the S-box to each of the four bytes to produce an output word. + + // Function Subword() + { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + + tempa[0] = tempa[0] ^ Rcon[i/Nk]; + } +#if defined(AES256) && (AES256 == 1) + if (i % Nk == 4) + { + // Function Subword() + { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + } +#endif + j = i * 4; k=(i - Nk) * 4; + RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; + RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; + RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; + RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; + } +} + +void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key) +{ + KeyExpansion(ctx->RoundKey, key); +} +#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv) +{ + KeyExpansion(ctx->RoundKey, key); + memcpy (ctx->Iv, iv, AES_BLOCKLEN); +} +void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv) +{ + memcpy (ctx->Iv, iv, AES_BLOCKLEN); +} +#endif + +// This function adds the round key to state. +// The round key is added to the state by an XOR function. +static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey) +{ + uint8_t i,j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; + } + } +} + +// The SubBytes Function Substitutes the values in the +// state matrix with values in an S-box. +static void SubBytes(state_t* state) +{ + uint8_t i, j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[j][i] = getSBoxValue((*state)[j][i]); + } + } +} + +// The ShiftRows() function shifts the rows in the state to the left. +// Each row is shifted with different offset. +// Offset = Row number. So the first row is not shifted. +static void ShiftRows(state_t* state) +{ + uint8_t temp; + + // Rotate first row 1 columns to left + temp = (*state)[0][1]; + (*state)[0][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[3][1]; + (*state)[3][1] = temp; + + // Rotate second row 2 columns to left + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + // Rotate third row 3 columns to left + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[3][3]; + (*state)[3][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[1][3]; + (*state)[1][3] = temp; +} + +static uint8_t xtime(uint8_t x) +{ + return ((x<<1) ^ (((x>>7) & 1) * 0x1b)); +} + +// MixColumns function mixes the columns of the state matrix +static void MixColumns(state_t* state) +{ + uint8_t i; + uint8_t Tmp, Tm, t; + for (i = 0; i < 4; ++i) + { + t = (*state)[i][0]; + Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ; + Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ; + Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ; + Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ; + Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ; + } +} + +// Multiply is used to multiply numbers in the field GF(2^8) +// Note: The last call to xtime() is unneeded, but often ends up generating a smaller binary +// The compiler seems to be able to vectorize the operation better this way. +// See https://github.com/kokke/tiny-AES-c/pull/34 +#if MULTIPLY_AS_A_FUNCTION +static uint8_t Multiply(uint8_t x, uint8_t y) +{ + return (((y & 1) * x) ^ + ((y>>1 & 1) * xtime(x)) ^ + ((y>>2 & 1) * xtime(xtime(x))) ^ + ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ + ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))); /* this last call to xtime() can be omitted */ + } +#else +#define Multiply(x, y) \ + ( ((y & 1) * x) ^ \ + ((y>>1 & 1) * xtime(x)) ^ \ + ((y>>2 & 1) * xtime(xtime(x))) ^ \ + ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \ + ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \ + +#endif + +#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) +/* +static uint8_t getSBoxInvert(uint8_t num) +{ + return rsbox[num]; +} +*/ +#define getSBoxInvert(num) (rsbox[(num)]) + +// MixColumns function mixes the columns of the state matrix. +// The method used to multiply may be difficult to understand for the inexperienced. +// Please use the references to gain more information. +static void InvMixColumns(state_t* state) +{ + int i; + uint8_t a, b, c, d; + for (i = 0; i < 4; ++i) + { + a = (*state)[i][0]; + b = (*state)[i][1]; + c = (*state)[i][2]; + d = (*state)[i][3]; + + (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09); + (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d); + (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b); + (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e); + } +} + + +// The SubBytes Function Substitutes the values in the +// state matrix with values in an S-box. +static void InvSubBytes(state_t* state) +{ + uint8_t i, j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[j][i] = getSBoxInvert((*state)[j][i]); + } + } +} + +static void InvShiftRows(state_t* state) +{ + uint8_t temp; + + // Rotate first row 1 columns to right + temp = (*state)[3][1]; + (*state)[3][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[0][1]; + (*state)[0][1] = temp; + + // Rotate second row 2 columns to right + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + // Rotate third row 3 columns to right + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[1][3]; + (*state)[1][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[3][3]; + (*state)[3][3] = temp; +} +#endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) + +// Cipher is the main function that encrypts the PlainText. +static void Cipher(state_t* state, const uint8_t* RoundKey) +{ + uint8_t round = 0; + + // Add the First round key to the state before starting the rounds. + AddRoundKey(0, state, RoundKey); + + // There will be Nr rounds. + // The first Nr-1 rounds are identical. + // These Nr rounds are executed in the loop below. + // Last one without MixColumns() + for (round = 1; ; ++round) + { + SubBytes(state); + ShiftRows(state); + if (round == Nr) { + break; + } + MixColumns(state); + AddRoundKey(round, state, RoundKey); + } + // Add round key to last round + AddRoundKey(Nr, state, RoundKey); +} + +#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) +static void InvCipher(state_t* state, const uint8_t* RoundKey) +{ + uint8_t round = 0; + + // Add the First round key to the state before starting the rounds. + AddRoundKey(Nr, state, RoundKey); + + // There will be Nr rounds. + // The first Nr-1 rounds are identical. + // These Nr rounds are executed in the loop below. + // Last one without InvMixColumn() + for (round = (Nr - 1); ; --round) + { + InvShiftRows(state); + InvSubBytes(state); + AddRoundKey(round, state, RoundKey); + if (round == 0) { + break; + } + InvMixColumns(state); + } + +} +#endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) + +/*****************************************************************************/ +/* Public functions: */ +/*****************************************************************************/ +#if defined(ECB) && (ECB == 1) + + +void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf) +{ + // The next function call encrypts the PlainText with the Key using AES algorithm. + Cipher((state_t*)buf, ctx->RoundKey); +} + +void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf) +{ + // The next function call decrypts the PlainText with the Key using AES algorithm. + InvCipher((state_t*)buf, ctx->RoundKey); +} + + +#endif // #if defined(ECB) && (ECB == 1) + + + + + +#if defined(CBC) && (CBC == 1) + + +static void XorWithIv(uint8_t* buf, const uint8_t* Iv) +{ + uint8_t i; + for (i = 0; i < AES_BLOCKLEN; ++i) // The block in AES is always 128bit no matter the key size + { + buf[i] ^= Iv[i]; + } +} + +void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, size_t length) +{ + size_t i; + uint8_t *Iv = ctx->Iv; + for (i = 0; i < length; i += AES_BLOCKLEN) + { + XorWithIv(buf, Iv); + Cipher((state_t*)buf, ctx->RoundKey); + Iv = buf; + buf += AES_BLOCKLEN; + } + /* store Iv in ctx for next call */ + memcpy(ctx->Iv, Iv, AES_BLOCKLEN); +} + +void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) +{ + size_t i; + uint8_t storeNextIv[AES_BLOCKLEN]; + for (i = 0; i < length; i += AES_BLOCKLEN) + { + memcpy(storeNextIv, buf, AES_BLOCKLEN); + InvCipher((state_t*)buf, ctx->RoundKey); + XorWithIv(buf, ctx->Iv); + memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN); + buf += AES_BLOCKLEN; + } + +} + +#endif // #if defined(CBC) && (CBC == 1) + + + +#if defined(CTR) && (CTR == 1) + +/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ +void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) +{ + uint8_t buffer[AES_BLOCKLEN]; + + size_t i; + int bi; + for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) + { + if (bi == AES_BLOCKLEN) /* we need to regen xor compliment in buffer */ + { + + memcpy(buffer, ctx->Iv, AES_BLOCKLEN); + Cipher((state_t*)buffer,ctx->RoundKey); + + /* Increment Iv and handle overflow */ + for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) + { + /* inc will overflow */ + if (ctx->Iv[bi] == 255) + { + ctx->Iv[bi] = 0; + continue; + } + ctx->Iv[bi] += 1; + break; + } + bi = 0; + } + + buf[i] = (buf[i] ^ buffer[bi]); + } +} + +#endif // #if defined(CTR) && (CTR == 1) + diff --git a/third_party/tinyaes/aes.h b/third_party/tinyaes/aes.h new file mode 100644 index 0000000..9098901 --- /dev/null +++ b/third_party/tinyaes/aes.h @@ -0,0 +1,97 @@ +#ifndef _AES_H_ +#define _AES_H_ + +#include "hybbx_aes_config.h" + +#include +#include + +// #define the macros below to 1/0 to enable/disable the mode of operation. +// +// CBC enables AES encryption in CBC-mode of operation. +// CTR enables encryption in counter-mode. +// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously. + +// The #ifndef-guard allows it to be configured before #include'ing or at compile time. +#ifndef CBC + #define CBC 1 +#endif + +#ifndef ECB + #define ECB 1 +#endif + +#ifndef CTR + #define CTR 1 +#endif + + +#ifndef AES128 +#define AES128 1 +#endif +//#define AES192 1 +#ifndef AES256 +//#define AES256 1 +#endif + +#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only + +#if defined(AES256) && (AES256 == 1) + #define AES_KEYLEN 32 + #define AES_keyExpSize 240 +#elif defined(AES192) && (AES192 == 1) + #define AES_KEYLEN 24 + #define AES_keyExpSize 208 +#else + #define AES_KEYLEN 16 // Key length in bytes + #define AES_keyExpSize 176 +#endif + +struct AES_ctx +{ + uint8_t RoundKey[AES_keyExpSize]; +#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) + uint8_t Iv[AES_BLOCKLEN]; +#endif +}; + +void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key); +#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); +void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); +#endif + +#if defined(ECB) && (ECB == 1) +// buffer size is exactly AES_BLOCKLEN bytes; +// you need only AES_init_ctx as IV is not used in ECB +// NB: ECB is considered insecure for most uses +void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf); +void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf); + +#endif // #if defined(ECB) && (ECB == !) + + +#if defined(CBC) && (CBC == 1) +// buffer size MUST be mutile of AES_BLOCKLEN; +// Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme +// NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv() +// no IV should ever be reused with the same key +void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); +void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); + +#endif // #if defined(CBC) && (CBC == 1) + + +#if defined(CTR) && (CTR == 1) + +// Same function for encrypting as for decrypting. +// IV is incremented for every block, and used after encryption as XOR-compliment for output +// Suggesting https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme +// NOTES: you need to set IV in ctx with AES_init_ctx_iv() or AES_ctx_set_iv() +// no IV should ever be reused with the same key +void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); + +#endif // #if defined(CTR) && (CTR == 1) + + +#endif // _AES_H_ diff --git a/third_party/tinyaes/aes256gcm.c b/third_party/tinyaes/aes256gcm.c new file mode 100644 index 0000000..32bf832 --- /dev/null +++ b/third_party/tinyaes/aes256gcm.c @@ -0,0 +1,267 @@ +/* + * AES-256-GCM for HyBBX (NIST SP 800-38D) using bundled tiny-AES-c. + */ + +#include "aes256gcm.h" +#include "aes.h" + +#include +#include + +typedef struct { + uint64_t hi; + uint64_t lo; +} u128; + +static u128 u128_from_be(const uint8_t in[16]) +{ + u128 v; + int i; + + v.hi = 0; + v.lo = 0; + for (i = 0; i < 8; i++) { + v.hi = (v.hi << 8) | in[i]; + v.lo = (v.lo << 8) | in[8 + i]; + } + return v; +} + +static void u128_to_be(u128 v, uint8_t out[16]) +{ + int i; + + for (i = 7; i >= 0; i--) { + out[i] = (uint8_t)(v.hi & 0xff); + v.hi >>= 8; + } + for (i = 15; i >= 8; i--) { + out[i] = (uint8_t)(v.lo & 0xff); + v.lo >>= 8; + } +} + +static void u128_xor(u128 *a, const u128 *b) +{ + a->hi ^= b->hi; + a->lo ^= b->lo; +} + +static void u128_shr1(u128 *v) +{ + int lsb = (int)(v->lo & 1); + + v->lo = (v->hi << 63) | (v->lo >> 1); + v->hi >>= 1; + if (lsb) { + v->hi ^= 0xe100000000000000ULL; + } +} + +static void gcm_gmult(uint8_t x[16], const uint8_t y[16]) +{ + u128 V = u128_from_be(y); + u128 Z = { 0, 0 }; + int i; + + for (i = 0; i < 128; i++) { + if (x[i / 8] & (0x80u >> (i % 8))) { + u128_xor(&Z, &V); + } + if (i == 127) { + break; + } + u128_shr1(&V); + } + + u128_to_be(Z, x); +} + +static void xor_block(uint8_t dst[16], const uint8_t a[16], const uint8_t b[16]) +{ + int i; + + for (i = 0; i < 16; i++) { + dst[i] = (uint8_t)(a[i] ^ b[i]); + } +} + +static void aes_ecb_encrypt_block(const struct AES_ctx *ctx, + const uint8_t in[16], uint8_t out[16]) +{ + uint8_t buf[16]; + + memcpy(buf, in, 16); + AES_ECB_encrypt((struct AES_ctx *)ctx, buf); + memcpy(out, buf, 16); +} + +static void ghash(const uint8_t h[16], const uint8_t *aad, size_t aad_len, + const uint8_t *cipher, size_t cipher_len, uint8_t out[16]) +{ + uint8_t y[16] = { 0 }; + uint8_t block[16]; + size_t i; + + for (i = 0; i + 16 <= aad_len; i += 16) { + xor_block(y, y, aad + i); + gcm_gmult(y, h); + } + if (i < aad_len) { + memset(block, 0, sizeof(block)); + memcpy(block, aad + i, aad_len - i); + xor_block(y, y, block); + gcm_gmult(y, h); + } + + for (i = 0; i + 16 <= cipher_len; i += 16) { + xor_block(y, y, cipher + i); + gcm_gmult(y, h); + } + if (i < cipher_len) { + memset(block, 0, sizeof(block)); + memcpy(block, cipher + i, cipher_len - i); + xor_block(y, y, block); + gcm_gmult(y, h); + } + + memset(block, 0, sizeof(block)); + for (i = 0; i < 8; i++) { + block[7 - i] = (uint8_t)((aad_len * 8) >> (i * 8)); + block[15 - i] = (uint8_t)((cipher_len * 8) >> (i * 8)); + } + xor_block(y, y, block); + gcm_gmult(y, h); + memcpy(out, y, 16); +} + +static void gctr(struct AES_ctx *ctx, const uint8_t icb[16], uint8_t *data, + size_t len) +{ + uint8_t counter[16]; + uint8_t stream[16]; + size_t i; + + memcpy(counter, icb, 16); + for (i = 0; i < len; i += 16) { + size_t j; + + aes_ecb_encrypt_block(ctx, counter, stream); + for (j = 0; j < 16 && i + j < len; j++) { + data[i + j] ^= stream[j]; + } + { + int k; + + for (k = 15; k >= 12; k--) { + counter[k] = (uint8_t)(counter[k] + 1); + if (counter[k] != 0) { + break; + } + } + } + } +} + +static void inc32(uint8_t block[16]) +{ + int i; + + for (i = 15; i >= 12; i--) { + block[i] = (uint8_t)(block[i] + 1); + if (block[i] != 0) { + break; + } + } +} + +static int aes256gcm_crypt(int encrypt, const uint8_t key[32], + const uint8_t nonce[12], const uint8_t *aad, + size_t aad_len, const uint8_t *in, size_t in_len, + uint8_t *out, const uint8_t tag_in[16], + uint8_t tag_out[16]) +{ + struct AES_ctx ctx; + uint8_t h[16] = { 0 }; + uint8_t j0[16]; + uint8_t s[16]; + uint8_t expected[16]; + int diff = 0; + size_t i; + + if (key == NULL || nonce == NULL) { + return -1; + } + if (in_len > 0 && (in == NULL || out == NULL)) { + return -1; + } + + AES_init_ctx(&ctx, key); + aes_ecb_encrypt_block(&ctx, h, h); + + memset(j0, 0, sizeof(j0)); + memcpy(j0, nonce, 12); + j0[15] = 1; + + if (in_len > 0) { + memcpy(out, in, in_len); + if (encrypt) { + memcpy(expected, j0, 16); + inc32(expected); + gctr(&ctx, expected, out, in_len); + } + } + + ghash(h, aad, aad_len, out, in_len, s); + memcpy(expected, j0, 16); + gctr(&ctx, expected, s, 16); + + if (encrypt) { + if (tag_out != NULL) { + memcpy(tag_out, s, 16); + } + return 0; + } + + if (tag_in == NULL) { + return -1; + } + + for (i = 0; i < 16; i++) { + diff |= (int)(s[i] ^ tag_in[i]); + } + if (diff != 0) { + return -1; + } + + if (in_len > 0) { + memcpy(expected, j0, 16); + inc32(expected); + memcpy(out, in, in_len); + gctr(&ctx, expected, out, in_len); + } + + return 0; +} + +int hybbx_aes256gcm_encrypt(const uint8_t key[HYBBX_AES256GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_AES256GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, + uint8_t tag[HYBBX_AES256GCM_TAG_SIZE]) +{ + return aes256gcm_crypt(1, key, nonce, aad, aad_len, plaintext, + plaintext_len, ciphertext, NULL, tag); +} + +int hybbx_aes256gcm_decrypt(const uint8_t key[HYBBX_AES256GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_AES256GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, + const uint8_t tag[HYBBX_AES256GCM_TAG_SIZE]) +{ + return aes256gcm_crypt(0, key, nonce, aad, aad_len, ciphertext, + ciphertext_len, plaintext, tag, NULL); +} diff --git a/third_party/tinyaes/aes256gcm.h b/third_party/tinyaes/aes256gcm.h new file mode 100644 index 0000000..c7a349a --- /dev/null +++ b/third_party/tinyaes/aes256gcm.h @@ -0,0 +1,32 @@ +#ifndef HYBBX_AES256GCM_H +#define HYBBX_AES256GCM_H + +#include +#include + +#define HYBBX_AES256GCM_KEY_SIZE 32 +#define HYBBX_AES256GCM_NONCE_SIZE 12 +#define HYBBX_AES256GCM_TAG_SIZE 16 + +/** + * AES-256-GCM encrypt. @p ciphertext length equals @p plaintext_len. + * @p tag receives the 16-byte authentication tag. + */ +int hybbx_aes256gcm_encrypt(const uint8_t key[HYBBX_AES256GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_AES256GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, + const uint8_t *plaintext, size_t plaintext_len, + uint8_t *ciphertext, + uint8_t tag[HYBBX_AES256GCM_TAG_SIZE]); + +/** + * AES-256-GCM decrypt. Returns 0 on success (tag valid), -1 on failure. + */ +int hybbx_aes256gcm_decrypt(const uint8_t key[HYBBX_AES256GCM_KEY_SIZE], + const uint8_t nonce[HYBBX_AES256GCM_NONCE_SIZE], + const uint8_t *aad, size_t aad_len, + const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext, + const uint8_t tag[HYBBX_AES256GCM_TAG_SIZE]); + +#endif diff --git a/third_party/tinyaes/hybbx_aes_config.h b/third_party/tinyaes/hybbx_aes_config.h new file mode 100644 index 0000000..f4fffd9 --- /dev/null +++ b/third_party/tinyaes/hybbx_aes_config.h @@ -0,0 +1,13 @@ +/* HyBBX tiny-AES-c configuration: AES-256, ECB + CTR for GCM. */ +#ifndef HYBBX_AES_CONFIG_H +#define HYBBX_AES_CONFIG_H + +#define CBC 0 +#define ECB 1 +#define CTR 1 +#undef AES128 +#define AES128 0 +#undef AES256 +#define AES256 1 + +#endif diff --git a/third_party/tinysha256/README.md b/third_party/tinysha256/README.md new file mode 100644 index 0000000..b4598ca --- /dev/null +++ b/third_party/tinysha256/README.md @@ -0,0 +1,8 @@ +# tinysha256 (bundled) + +Public-domain SHA-256 for HyBBX password hashing. + +- `tinysha256.c` / `tinysha256.h` — compiled into `hybbx_core` +- Adapted from the [983/SHA-256](https://github.com/983/SHA-256) implementation (The Unlicense) + +HyBBX stores passwords as `{sha256}` + 64 lowercase hex digits. diff --git a/third_party/tinysha256/tinysha256.c b/third_party/tinysha256/tinysha256.c new file mode 100644 index 0000000..d1b2019 --- /dev/null +++ b/third_party/tinysha256/tinysha256.c @@ -0,0 +1,210 @@ +#include "tinysha256.h" + +static inline uint32_t tinysha256_rotr(uint32_t x, int n) +{ + return (x >> n) | (x << (32 - n)); +} + +static inline uint32_t tinysha256_step1(uint32_t e, uint32_t f, uint32_t g) +{ + return (tinysha256_rotr(e, 6) ^ tinysha256_rotr(e, 11) ^ tinysha256_rotr(e, 25)) + + ((e & f) ^ ((~e) & g)); +} + +static inline uint32_t tinysha256_step2(uint32_t a, uint32_t b, uint32_t c) +{ + return (tinysha256_rotr(a, 2) ^ tinysha256_rotr(a, 13) ^ tinysha256_rotr(a, 22)) + + ((a & b) ^ (a & c) ^ (b & c)); +} + +static inline void tinysha256_update_w(uint32_t *w, int i, const uint8_t *buffer) +{ + int j; + + for (j = 0; j < 16; j++) { + if (i < 16) { + w[j] = ((uint32_t)buffer[0] << 24) | + ((uint32_t)buffer[1] << 16) | + ((uint32_t)buffer[2] << 8) | + ((uint32_t)buffer[3]); + buffer += 4; + } else { + uint32_t a = w[(j + 1) & 15]; + uint32_t b = w[(j + 14) & 15]; + uint32_t s0 = (tinysha256_rotr(a, 7) ^ tinysha256_rotr(a, 18) ^ (a >> 3)); + uint32_t s1 = (tinysha256_rotr(b, 17) ^ tinysha256_rotr(b, 19) ^ (b >> 10)); + + w[j] += w[(j + 9) & 15] + s0 + s1; + } + } +} + +static void tinysha256_block(tinysha256_ctx_t *ctx) +{ + uint32_t *state = ctx->state; + + static const uint32_t k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + + uint32_t a = state[0]; + uint32_t b = state[1]; + uint32_t c = state[2]; + uint32_t d = state[3]; + uint32_t e = state[4]; + uint32_t f = state[5]; + uint32_t g = state[6]; + uint32_t h = state[7]; + uint32_t w[16]; + int i; + int j; + + for (i = 0; i < 64; i += 16) { + tinysha256_update_w(w, i, ctx->buffer); + + for (j = 0; j < 16; j += 4) { + uint32_t temp; + + temp = h + tinysha256_step1(e, f, g) + k[i + j + 0] + w[j + 0]; + h = temp + d; + d = temp + tinysha256_step2(a, b, c); + temp = g + tinysha256_step1(h, e, f) + k[i + j + 1] + w[j + 1]; + g = temp + c; + c = temp + tinysha256_step2(d, a, b); + temp = f + tinysha256_step1(g, h, e) + k[i + j + 2] + w[j + 2]; + f = temp + b; + b = temp + tinysha256_step2(c, d, a); + temp = e + tinysha256_step1(f, g, h) + k[i + j + 3] + w[j + 3]; + e = temp + a; + a = temp + tinysha256_step2(b, c, d); + } + } + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + state[5] += f; + state[6] += g; + state[7] += h; +} + +void tinysha256_init(tinysha256_ctx_t *ctx) +{ + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; + ctx->n_bits = 0; + ctx->buffer_counter = 0; +} + +static void tinysha256_append_byte(tinysha256_ctx_t *ctx, uint8_t byte) +{ + ctx->buffer[ctx->buffer_counter++] = byte; + ctx->n_bits += 8; + + if (ctx->buffer_counter == 64) { + ctx->buffer_counter = 0; + tinysha256_block(ctx); + } +} + +void tinysha256_append(tinysha256_ctx_t *ctx, const void *src, size_t n_bytes) +{ + const uint8_t *bytes = (const uint8_t *)src; + size_t i; + + for (i = 0; i < n_bytes; i++) { + tinysha256_append_byte(ctx, bytes[i]); + } +} + +static void tinysha256_finalize(tinysha256_ctx_t *ctx) +{ + int i; + uint64_t n_bits = ctx->n_bits; + + tinysha256_append_byte(ctx, 0x80); + + while (ctx->buffer_counter != 56) { + tinysha256_append_byte(ctx, 0); + } + + for (i = 7; i >= 0; i--) { + uint8_t byte = (uint8_t)((n_bits >> (8 * i)) & 0xff); + + tinysha256_append_byte(ctx, byte); + } +} + +void tinysha256_finalize_hex(tinysha256_ctx_t *ctx, char *dst_hex65) +{ + int i; + int j; + + tinysha256_finalize(ctx); + + for (i = 0; i < 8; i++) { + for (j = 7; j >= 0; j--) { + uint8_t nibble = (uint8_t)((ctx->state[i] >> (j * 4)) & 0xf); + + *dst_hex65++ = "0123456789abcdef"[nibble]; + } + } + + *dst_hex65 = '\0'; +} + +void tinysha256_finalize_bytes(tinysha256_ctx_t *ctx, void *dst_bytes32) +{ + uint8_t *ptr = (uint8_t *)dst_bytes32; + int i; + int j; + + tinysha256_finalize(ctx); + + for (i = 0; i < 8; i++) { + for (j = 3; j >= 0; j--) { + *ptr++ = (uint8_t)((ctx->state[i] >> (j * 8)) & 0xff); + } + } +} + +void tinysha256_hex(const void *src, size_t n_bytes, char *dst_hex65) +{ + tinysha256_ctx_t ctx; + + tinysha256_init(&ctx); + tinysha256_append(&ctx, src, n_bytes); + tinysha256_finalize_hex(&ctx, dst_hex65); +} + +void tinysha256_bytes(const void *src, size_t n_bytes, void *dst_bytes32) +{ + tinysha256_ctx_t ctx; + + tinysha256_init(&ctx); + tinysha256_append(&ctx, src, n_bytes); + tinysha256_finalize_bytes(&ctx, dst_bytes32); +} diff --git a/third_party/tinysha256/tinysha256.h b/third_party/tinysha256/tinysha256.h new file mode 100644 index 0000000..2e9ae96 --- /dev/null +++ b/third_party/tinysha256/tinysha256.h @@ -0,0 +1,30 @@ +#ifndef TINYSHA256_H +#define TINYSHA256_H + +/* + * tinysha256 — public-domain SHA-256 for HyBBX (bundled, no external deps). + * Adapted from the Unlicense implementation by 983 (https://github.com/983/SHA-256). + */ + +#include +#include + +#define TINYSHA256_HEX_SIZE (64 + 1) +#define TINYSHA256_BYTES_SIZE 32 + +void tinysha256_hex(const void *src, size_t n_bytes, char *dst_hex65); +void tinysha256_bytes(const void *src, size_t n_bytes, void *dst_bytes32); + +typedef struct tinysha256_ctx { + uint32_t state[8]; + uint8_t buffer[64]; + uint64_t n_bits; + uint8_t buffer_counter; +} tinysha256_ctx_t; + +void tinysha256_init(tinysha256_ctx_t *ctx); +void tinysha256_append(tinysha256_ctx_t *ctx, const void *data, size_t n_bytes); +void tinysha256_finalize_hex(tinysha256_ctx_t *ctx, char *dst_hex65); +void tinysha256_finalize_bytes(tinysha256_ctx_t *ctx, void *dst_bytes32); + +#endif /* TINYSHA256_H */ -- cgit v1.3.1