diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 | |
#2
267 files changed, 60955 insertions, 0 deletions
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 <info@un1t.me>` — 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 (`/<verb> …` + `Help: …`); headers use `/help <cmd> 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 <info@un1t.me>` 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 <repository-url> && 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 <https://www.gnu.org/licenses/>. + +------------------------------------------------------------------------ + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + 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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <https://www.gnu.org/licenses/>. + +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: + + <program> Copyright (C) <year> <name of author> + 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 +<https://www.gnu.org/licenses/>. + + 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 +<https://www.gnu.org/licenses/why-not-lgpl.html>. 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) | + +--- +<div align="center"> +<sub> +<strong>UN1TME/UN1T.me · BBX</strong> +<br><br> +<table> +<tr><th>Protocol</th><th>Host</th><th>Port</th><th>Command / URL</th></tr> +<tr><td align="center">Telnet</td><td align="center"><code>hybbx.un1t.me</code></td><td align="center">2323</td><td align="center"><code>telnet hybbx.un1t.me 2323</code></td></tr> +<tr><td align="center">SSH</td><td align="center"><code>hybbx.un1t.me</code></td><td align="center">3232</td><td align="center"><code>ssh hybbx.un1t.me -p 3232</code></td></tr> +<tr><td align="center">HTTPS</td><td align="center"><code>hybbx.un1t.me</code></td><td align="center">443</td><td align="center"><a href="https://hybbx.un1t.me/">https://hybbx.un1t.me/</a></td></tr> +<tr><td align="center">HTTP</td><td align="center"><code>hybbx.un1t.me</code></td><td align="center">80</td><td align="center"><a href="http://hybbx.un1t.me/">http://hybbx.un1t.me/</a></td></tr> +</table> +<br> +<strong>hybbx.un1t.me</strong> Telnet <strong>:2323</strong> · SSH <strong>:3232</strong> · HTTP(S) <strong>:80/:443</strong> · Primary RF <strong>27.245 MHz / CB25</strong> +</sub> +<br><br> +<a href="https://github.com/user-attachments/assets/0429f9e3-61b0-4b21-a01a-dfb98eabce77"> <img width="80" height="60" alt="UN1TME/UN1T.me BBX — sysop menu" src="https://github.com/user-attachments/assets/0429f9e3-61b0-4b21-a01a-dfb98eabce77" /> +</a> +</div> 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 <msg>` | 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 @@ +<!-- AUTO-SYNC 2026-08-01 — vault: projects/integration/2026-07-13-master-operator-guide.md --> +<!-- Re-sync: /home/akb/Code/0-RESEARCHES/tools/vault-sync-slave-docs.sh --> + +# 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 @@ +<!-- AUTO-SYNC 2026-08-01 — vault: projects/hybbx/2026-07-13-master-documentation.md --> +<!-- Re-sync: /home/akb/Code/0-RESEARCHES/tools/vault-sync-slave-docs.sh --> + +# 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 <msg>` | 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 <msg>` | 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 <stddef.h> +#include <stdint.h> + +#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 <guest_prefix><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 <stddef.h> +#include <stdint.h> + +#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: `+++ <from>: <message>` (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 "<username>: …". + */ +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 <stddef.h> +#include <stdint.h> + +#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 <stddef.h> +#include <stdint.h> +#include <time.h> + +#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 + * /<verb> or / <verb> → command + * /command <verb> or /cmd <verb> → same as / <verb> + * + * - 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 <stddef.h> +#include <stdint.h> + +#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 <stddef.h> +#include <stdint.h> + +#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/<id>.ini ; [link.<id>] in hybbx.ini. + */ + +#include "hybbx/types.h" +#include "hybbx/limits.h" + +#include <stddef.h> +#include <time.h> + +#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/<id>.ini and @c [link.<id>] 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 <stddef.h> + +#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 <time.h> + +#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 <storage>/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: `*** <body>` + */ +hybbx_result_t hybbx_msg_format_system(char *out, size_t out_len, + const char *body); + +/** + * Format a Sysop/granted system-wide line: `+++ <from>: <body>` + * @p from_label may be NULL → `+++ <body>` + */ +hybbx_result_t hybbx_msg_format_sysop(char *out, size_t out_len, + const char *from_label, + const char *body); + +/** + * Format a private line: `### <from>: <body>` or `### <body>` 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 <stddef.h> + +#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}<hex> 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 <time.h> / <unistd.h> 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 <time.h> + +#if !defined(_WIN32) +#include <unistd.h> +#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 <stddef.h> + +#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 <sys/socket.h> + +#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 <stddef.h> +#include <stdint.h> +#include <time.h> + +#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.<id>]. */ +#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 <stddef.h> + +#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 <stddef.h> + +#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 <stddef.h> +#include <stdint.h> + +#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 <stddef.h> +#include <time.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <stddef.h> + +/** 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 <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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 <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#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 <pthread.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(__linux__) + +#include <arpa/inet.h> +#include <fcntl.h> +#include <net/if.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <linux/if_packet.h> +#include <linux/if_ether.h> +#include <linux/hdlcdrv.h> + +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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +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 <stdlib.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <ctype.h> +#include <stdlib.h> +#include <string.h> + +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 <string.h> + +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 <pthread.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <errno.h> +#include <poll.h> +#include <sys/select.h> +#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 <stdlib.h> +#include <string.h> + +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 <stdlib.h> +#include <string.h> +#include <stdio.h> + +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 <windows.h> + +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 <proto/exec.h> +#include <devices/serial.h> + +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 <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <string.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#if !defined(_WIN32) +#include <sys/select.h> +#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 <ctype.h> +#include <string.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <stdio.h> + +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 <libssh/callbacks.h> +#include <libssh/libssh.h> +#include <libssh/server.h> + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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 <stdlib.h> +#include <string.h> + +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 <libssh/libssh.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +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 <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +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 <stdlib.h> +#include <string.h> + +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 <errno.h> +#include <string.h> +#include <unistd.h> + +#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 <stddef.h> +#include <stdint.h> + +#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 <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#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 <stdlib.h> +#include <string.h> + +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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/socket.h> +#include <unistd.h> + +#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 <stddef.h> +#include <stdint.h> + +#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 <stdint.h> +#include <string.h> +#include <string.h> + +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 <stddef.h> +#include <stdint.h> + +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 <errno.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef HYBBX_WS_HAVE_TLS + +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/ssl.h> +#include <openssl/x509.h> + +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 <stddef.h> +#include <sys/types.h> + +#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: <tree>/hybbx-start + <tree>/hybbxd (./bin or <prefix>/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" <<EOF +[service] +name = test-ardop + +[storage] +path = ${TMPDIR}/data + +[auth] +auto_login = no + +[networks] +ax25 = no +ardop = yes +circuit = no + +[transport.telnet] +enabled = no + +[transport.ardop] +enabled = yes +ardop_host = 127.0.0.1 +ardop_port = ${MOCK_PORT} +mycall = TEST-0 +radio_profile = cb +arq_bandwidth = 500MAX +listen = yes +circuit_host = 127.0.0.1 +circuit_port = 17323 +link_id = test-ardop +EOF + +python3 "${ROOT}/scripts/mock-ardopc.py" "${MOCK_PORT}" & +MOCK_PID=$! +sleep 0.3 + +# Minimal TCP acceptor so circuit connect does not block startup (no HBX handshake). +nc -l 127.0.0.1 17323 >/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" <<EOF +[service] +name = test-crdop + +[storage] +path = ${TMPDIR}/data + +[auth] +auto_login = no + +[networks] +ax25 = no +ardop = no +crdop = yes +circuit = no + +[transport.telnet] +enabled = no + +[transport.crdop] +enabled = yes +modem_host = 127.0.0.1 +modem_port = ${MOCK_PORT} +mycall = CB-0 +listen = yes +circuit_host = 127.0.0.1 +circuit_port = 17325 +link_id = test-crdop +EOF + +python3 "${ROOT}/scripts/mock-ardopc.py" "${MOCK_PORT}" & +MOCK_PID=$! +sleep 0.3 + +nc -l 127.0.0.1 17325 >/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 <cmd> for more" + index_header: "HyBBX command-index /help <cmd> for more" + alias_header: "HyBBX aliases /help <cmd> 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): +# /<verb> 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 <cmd>. 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 --- /dev/null +++ b/share/data.gitkeep 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=<HOST> .*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=<HOST> 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=<HOST> 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=<HOST> 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 --- /dev/null +++ b/share/logs.gitkeep 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 + +<IfModule mod_proxy.c> + 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" +</IfModule> + +Alias /hybbx-websocket /srv/www/hybbx-websocket +<Directory /srv/www/hybbx-websocket> + DirectoryIndex index.php + Require all granted + <FilesMatch \.php$> + SetHandler application/x-httpd-php + </FilesMatch> +</Directory> + +# 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 @@ +<?php +/** + * HyBBX browser terminal — copy this folder to your httpd document root. + * WebSocket data comes from hybbx only; this file is not part of the daemon. + */ +$base = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/'); +$ws_path = ($base !== '' ? $base : '/hybbx-websocket') . '/ws'; +$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'wss' : 'ws'; +$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; +$ws_url = $scheme . '://' . $host . $ws_path; +?><!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>HyBBX</title> + <style> + html, body { height: 100%; margin: 0; } + body { + display: flex; + flex-direction: column; + background: #0a0a12; + color: #c8d0d8; + font-family: ui-monospace, "Cascadia Mono", "Consolas", monospace; + } + #status { + position: fixed; + top: 0; + right: 0; + padding: .5rem 1rem; + font-size: .8rem; + background: #1a1a28; + z-index: 1; + } + #term { + flex: 1; + min-height: 0; + box-sizing: border-box; + width: 100%; + margin: 0; + padding: 1rem; + padding-bottom: .5rem; + white-space: pre-wrap; + overflow-y: auto; + overflow-x: hidden; + } + #input-bar { + flex: none; + display: flex; + width: 100%; + box-sizing: border-box; + border-top: 1px solid #2a2a38; + background: #12121c; + } + #cmd { + flex: 1; + width: 100%; + min-width: 0; + box-sizing: border-box; + border: none; + border-radius: 0; + background: #12121c; + color: #e0e4e8; + font: inherit; + font-size: 1rem; + line-height: 1.4; + padding: .75rem 1rem; + outline: none; + } + #cmd::placeholder { color: #6a7280; } + #cmd:focus { background: #161622; } + #cmd:disabled { opacity: .55; cursor: not-allowed; } + </style> +</head> +<body> + <div id="status">disconnected</div> + <pre id="term"></pre> + <form id="input-bar" autocomplete="off"> + <input type="text" id="cmd" disabled + autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" + enterkeyhint="send" + placeholder="Type command or text — Enter to send"> + </form> + <script> + window.HYBBX_WS_URL = <?php echo json_encode($ws_url, JSON_UNESCAPED_SLASHES); ?>; + </script> + <script src="hybbx-terminal.js"></script> +</body> +</html> 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 + $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include> + $<INSTALL_INTERFACE:${HYBBX_INSTALL_HOME}/include> + 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 + $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include> + 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 <arpa/inet.h> +#include <errno.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#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 <stdio.h> +#include <unistd.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <errno.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +#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 <stddef.h> +#include <termios.h> + +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 <libssh/libssh.h> + +#include <errno.h> +#include <getopt.h> +#include <poll.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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 <errno.h> +#include <getopt.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(__AMIGA__) +#include <arpa/inet.h> +#include <netdb.h> +#include <sys/select.h> +#include <sys/socket.h> +#else +#include <netdb.h> +#include <poll.h> +#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 <errno.h> +#include <getopt.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +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 <stdio.h> +#include <string.h> +#include <time.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +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 <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <n> or <name>."); + } +} + +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 <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <time.h> +#include <unistd.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +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 <username>"); + 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 <username> 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 <username>"); + 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 <username>"); + 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 <username> <full-name> <country> <location> <email>"); + return HYBBX_OK; + } + } else if (cmd->argc < 6) { + hybbx_session_write_line(session, + "Usage: /register <username> <full-name> <country> <location> <email> <password>"); + 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 <oldpass> <newpass> <full-name> <country> <location> <email>"); + 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 <user> <newpass> <full-name> <country> <location> <email>"); + 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 <username>"); + 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 <username> <password>"); + 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 <username> <password>"); + 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 <topic> <user>"); + 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 [<from>-<to>]"); + 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 <n|from-to>"); + return HYBBX_OK; + } + + if (!hybbx_mail_parse_list_range(cmd->argv[1], &from, &to)) { + hybbx_session_write_line(session, + "Usage: /proxymail delete <n|from-to>"); + 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 <n>"); + return HYBBX_OK; + } + + index = (unsigned)strtoul(cmd->argv[1], NULL, 10); + if (index == 0) { + hybbx_session_write_line(session, "Usage: /proxymail read <n>"); + 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 <user>@<main> <subject>"); + return HYBBX_OK; + } + + mail_join_subject(cmd, subject, sizeof(subject)); + if (subject[0] == '\0') { + hybbx_session_write_line(session, + "Usage: /proxymail send <user>@<main> <subject>"); + 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 [<from>-<to>] 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 <n|from-to> 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 <n|from-to> 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 <n>"); + return HYBBX_OK; + } + + index = (unsigned)strtoul(cmd->argv[1], NULL, 10); + if (index == 0) { + hybbx_session_write_line(session, "Usage: /mail read <n>"); + 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 <user> <subject>"); + return HYBBX_OK; + } + + mail_join_subject(cmd, subject, sizeof(subject)); + if (subject[0] == '\0') { + hybbx_session_write_line(session, + "Usage: /mail send <user> <subject>"); + 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 <user> [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 <user> [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 <user> [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 <ctype.h> +#include <stdlib.h> +#include <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +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 <stdio.h> +#include <string.h> +#include <time.h> + +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 <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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 <ctype.h> +#include <string.h> + +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 <string.h> + +#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 <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#if defined(__linux__) +#include <sys/random.h> +#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 <stddef.h> +#include <stdint.h> + +#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 <ctype.h> +#include <stdio.h> +#include <string.h> + +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 <sodium.h> + +#include <stdlib.h> +#include <string.h> + +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 <openssl/evp.h> +#include <openssl/rand.h> + +#include <stdio.h> +#include <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +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 <string.h> + +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/<id>.ini, [link.<id>], stale prune. + */ +#include "hybbx/link.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +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 <dirent.h> +#include <errno.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#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/<YYYYMMDD>-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 <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> + +#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 <user> <subject> 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 <n> delete <n|from-to> list <from-to> 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 <n|from-to>"); + 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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#ifdef HYBBX_HAVE_SQLITE +#include <sqlite3.h> + +#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 <errno.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +/* 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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <arpa/inet.h> +#include <fcntl.h> +#include <netinet/in.h> +#include <poll.h> +#include <sys/socket.h> +#include <unistd.h> +#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 <stdio.h> +#include <string.h> + +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 <stdio.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/types.h> + +#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 <stdio.h> +#include <string.h> + +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 <ctype.h> +#include <stdio.h> +#include <stdint.h> +#include <string.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#if !defined(_WIN32) +#include <errno.h> +#include <grp.h> +#include <pwd.h> +#include <sys/types.h> +#include <unistd.h> +#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 <stdio.h> +#include <string.h> + +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 <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +#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 <n> delete <n|from-to> list <from-to>"); +} + +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 <string.h> + +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 <pthread.h> + +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 <errno.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +#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 <ctype.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +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 <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#if defined(_WIN32) +#include <windows.h> +#else +#include <unistd.h> +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <time.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> + +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 <stddef.h> + +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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#ifdef HYBBX_HAVE_SQLITE +#include <sqlite3.h> +#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 <string.h> + +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 <stdio.h> +#include <string.h> +#include <time.h> + +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 <stdio.h> +#include <string.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#include <sys/time.h> +#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 <ctype.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <sys/socket.h> +#endif + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <sys/utsname.h> +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <libgen.h> + +#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 <file> 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 <binary>/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 <hybbx.ini>, 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 $<TARGET_FILE:hybbxd>) +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 <arpa/inet.h> +#include <sys/socket.h> + +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#include <dirent.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +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 +// <https://creativecommons.org/publicdomain/zero/1.0/> + +#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 +// <https://creativecommons.org/publicdomain/zero/1.0/> + +#ifndef MONOCYPHER_H +#define MONOCYPHER_H + +#include <stddef.h> +#include <stdint.h> + +#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 <string.h> // 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 <stdint.h> +#include <stddef.h> + +// #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 <stdint.h> +#include <string.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <stddef.h> +#include <stdint.h> + +#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 */ |
