Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
91068ef
feat(server): poll() event loop on POSIX, lifting the 1024-socket cei…
Aybook Jul 15, 2026
94a130f
Remove redundant save call in setlevel function (#437)
Kcchouette Jul 16, 2026
816eaf9
Refactor bool function in adc.lua (#438)
Kcchouette Jul 16, 2026
eddacc2
Fix type check for number in util.lua (#439)
Kcchouette Jul 16, 2026
1fbf735
docs: add CONTRIBUTING.md documenting the dev-branch policy (#440)
Aybook Jul 16, 2026
4c033e4
fix(sysinfo): CIM -> WMI fallback so old Windows (PS 2.0) reports OS/…
Aybook Jul 16, 2026
852270c
fix(ci): install zlib dev package on all three release legs (#441)
Aybook Jul 16, 2026
bc4d558
fix(scripts): resolve plugin lang lookups; guard the class repo-wide …
Aybook Jul 16, 2026
c42b1e4
fix(cmd_ban): reject a bantime below 1 instead of silently losing the…
Aybook Jul 16, 2026
27d4ed2
docs: correct hci, the event-loop backend, and the logsafe cross-refe…
Aybook Jul 16, 2026
6e15078
core/hub.lua: remove 19 dead file-scope locals (#448)
Aybook Jul 17, 2026
4762677
core: remove the doc.lua auto-doc generator (#449)
Aybook Jul 17, 2026
3520127
core/server.lua: remove unambiguously dead, core-internal code (#450)
Aybook Jul 17, 2026
86cbe6d
core/server.lua: remove the outbound-TCP wrapper and the STARTTLS sea…
Aybook Jul 17, 2026
d0f72cf
core: remove unreachable branches in util.lua and adc.lua (#452)
Aybook Jul 17, 2026
6a50569
core: remove 61 dead file-scope locals across 24 modules (#454)
Aybook Jul 17, 2026
4bf8b16
docs: record the removal-side use-trap and how to prove a core remova…
Aybook Jul 17, 2026
b8c14d5
scripts: remove 18 dead lang locals and 22 orphaned lang keys (#458)
Aybook Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ jobs:
- uses: actions/checkout@v4

- name: Install build dependencies
# zlib1g-dev is REQUIRED, not optional: CMakeLists.txt does
# find_package(ZLIB REQUIRED) for the zlib_stream module, and the
# dev package is what ships zlib.h + the libz.so link. The runtime
# zlib1g is present on every box (half of userspace links it), but
# build-essential does NOT depend on zlib1g-dev - so configure fails
# without this line. Keep in sync with smoke.yml + docker/Dockerfile.
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake libssl-dev
sudo apt-get install -y build-essential cmake libssl-dev zlib1g-dev

- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
Expand Down Expand Up @@ -129,7 +135,7 @@ jobs:
done
apt-get install -y --no-install-recommends \
build-essential perl wget ca-certificates \
patchelf file git
patchelf file git zlib1g-dev

- name: Install CMake ${{ env.CMAKE_VERSION }} (Kitware aarch64 binary)
# Bullseye's stock cmake (3.18) is too old for cmake_minimum_required(3.20).
Expand Down Expand Up @@ -291,6 +297,7 @@ jobs:
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-make
mingw-w64-ucrt-x86_64-openssl
mingw-w64-ucrt-x86_64-zlib
zip

- name: Configure
Expand Down
30 changes: 21 additions & 9 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
- name: Run bloom unit test
run: lua5.4 tests/unit/bloom_test.lua

- name: Run sysinfo unit test
run: lua5.4 tests/unit/sysinfo_test.lua

# Phase 1b of #82 HTTP API: router unit tests cover the pure-Lua
# bits (constant_time_eq, schema validator, envelope helpers,
# token resolution, request-id shape, register guards).
Expand Down Expand Up @@ -112,13 +115,18 @@ jobs:
- name: Run lang unit test
run: lua5.4 tests/unit/lang_test.lua

# #301 PR-2: usr_share lang-key consistency. Scans the plugin
# source for every lang.X reference and asserts X is defined in
# both .lang.de and .lang.en. Catches typos like the pre-fix
# lang.msg_minmax / msg_sharelimits drift that silently dropped
# the German translation.
- name: Run usr_share lang unit test
run: lua5.4 tests/unit/usr_share_lang_test.lua
# Repo-wide plugin lang-key consistency. For every bundled plugin
# that ships lang files, scans the source for each lang.X reference
# and asserts X is defined in both .lang.de and .lang.en. Catches
# the drift class that silently kills a translation: the lookup
# returns nil, the `or "<english>"` fallback fires, and no error is
# ever raised. Supersedes the former usr_share-only test (#301 PR-2,
# lang.msg_minmax vs msg_sharelimits) - usr_share is one of the ~68
# plugins swept here, so coverage is a strict superset. Generalised
# after the same bug reappeared in etc_cmdlog (lang.failmsg1/2 vs
# msg_denied/msg_nofile), which the one-plugin test could not see.
- name: Run plugin lang unit test
run: lua5.4 tests/unit/plugin_lang_test.lua

# #327 etc_aliases: HTTP API surface + reload-safety. Exercises
# all four +addalias reject branches (bad_alias / conflict_command
Expand Down Expand Up @@ -366,6 +374,10 @@ jobs:
shell: msys2 {0}
run: lua5.4 tests/unit/bloom_test.lua

- name: Run sysinfo unit test
shell: msys2 {0}
run: lua5.4 tests/unit/sysinfo_test.lua

- name: Run http_router unit test
shell: msys2 {0}
run: lua5.4 tests/unit/http_router_test.lua
Expand Down Expand Up @@ -406,9 +418,9 @@ jobs:
shell: msys2 {0}
run: lua5.4 tests/unit/lang_test.lua

- name: Run usr_share lang unit test
- name: Run plugin lang unit test
shell: msys2 {0}
run: lua5.4 tests/unit/usr_share_lang_test.lua
run: lua5.4 tests/unit/plugin_lang_test.lua

- name: Run etc_aliases unit test
shell: msys2 {0}
Expand Down
30 changes: 29 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

58 changes: 43 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ dependency updates are manual.
hub/hub.c ── luaL_newstate(), register C functions, load core/init.lua
└─ core/init.lua ── restricted env, load libs + _core modules in order
└─ core/hub.lua ── hub.loop() — main event loop
├─ core/server.lua ── select() loop over sockets, SSL wrap
├─ core/server.lua ── event loop over sockets (poll on POSIX,
│ select on Windows - #310), SSL wrap
└─ core/hub_dispatch.lua ── per-command ADC dispatch (most
listener events fire from here)
```
Expand All @@ -130,17 +131,33 @@ order (its inline comments explain each ordering constraint).
| Subsystem | Modules | Responsibility |
|---|---|---|
| Boot + config | `init`, `const`, `cfg`, `cfg_defaults`, `cfg_users`, `cfg_lang`, `cfg_secret`, `secrets` | Restricted env + module loader; program constants; settings/user.tbl/language handling; AES-256-GCM at-rest crypto; env-var-first secret lookup |
| Network + ADC | `server`, `iostream`, `adc`, `hub`, `hub_dispatch`, `hub_user_object`, `hub_bot_object`, `hbri`, `ratelimit`, `blocklist`, `whitelist`, `ipmatch` | select() loop + SSL; framing pipeline; ADC parse/escape/format; main loop + login; command dispatch; user/bot objects; dual-stack secondary-IP verification; DoS limits; pre-handshake IP/CIDR blocklist; global allowlist (whitelist beats automated blocks, not manual pins); IP/CIDR primitives |
| Network + ADC | `server`, `iostream`, `adc`, `hub`, `hub_dispatch`, `hub_user_object`, `hub_bot_object`, `hbri`, `ratelimit`, `blocklist`, `whitelist`, `ipmatch` | event loop + SSL (poll on POSIX / select on Windows, #310); framing pipeline; ADC parse/escape/format; main loop + login; command dispatch; user/bot objects; dual-stack secondary-IP verification; DoS limits; pre-handshake IP/CIDR blocklist; global allowlist (whitelist beats automated blocks, not manual pins); IP/CIDR primitives |
| HTTP API | `http`, `http_router`, `http_client`, `http_filter`, `http_events`, `util_http` | Inbound HTTP/JSON API + router + auth; non-blocking OUTBOUND client; filter/sort/paginate helper; deferred-event endpoints; plugin endpoint helper |
| Crypto + boot trust | `sha256`, `hmac`, `cert_bootstrap`, `cacert_bootstrap` | Pure-Lua SHA-256; HMAC-SHA256 (RFC 2104, sandbox-exposed for signed-webhook auth, #398); first-boot TLS-cert auto-gen (#77); CA-bundle reconciliation |
| Infra | `util`, `out`, `mem`, `signal`, `types`, `scripts`, `audit`, `sysinfo`, `mmdb`, `geoip_update`, `bloom`, `ensuredirs`, `doc` (disabled), `hci` (stub) | File I/O + table helpers; logging; GC; timers; ADC type validation; plugin loader + sandbox + listener registry; onAudit JSONL log; system info; MaxMind DB reader + in-hub GeoLite2 auto-update; bloom filter; boot-time runtime-dir self-heal; dormant |
| Infra | `util`, `out`, `mem`, `signal`, `types`, `scripts`, `audit`, `sysinfo`, `mmdb`, `geoip_update`, `bloom`, `ensuredirs` | File I/O + table helpers; logging; GC; timers; ADC type validation; plugin loader + sandbox + listener registry; onAudit JSONL log; system info; MaxMind DB reader + in-hub GeoLite2 auto-update; bloom filter; boot-time runtime-dir self-heal |

**`core/hci.lua` is not a module** - it is a persisted data file (a plain
`hubruntime` / `hubruntime_last_check` table) read and rewritten via
`util.loadtable` / `util.savetable` by `hub_runtime` (60s `onTimer`),
`cmd_uptime` and `cmd_hubinfo`, and it backs `/v1/runtime`. Its absence from
`_core` is correct - do not "fix" it by adding it. It is however the one piece
of mutable plugin state living under `core/` instead of `scripts/data/`,
contra §7 - and because `core/` is shipped wholesale (`install(DIRECTORY
core/)`, and Docker bakes it into the image rather than mounting it), every
upgrade overwrites the operator's accumulated runtime with the pristine
zeros: [#445](https://github.com/luadch-ng/luadch/issues/445). Not a dead
file - a misplaced one.

Two hard ceilings (both enforced by review, not tooling):

- **1500 lines per code module** (Phase 6). If a module needs more, split it.
- **`core/hub.lua` main chunk is AT Lua's 200-locals cap.** Any new top-level
`local` fails the build. Use lazy `use "X"` at call sites instead; treat
hub.lua file-scope locals as frozen.
- **`core/hub.lua`'s main chunk runs close to Lua's 200-locals-per-chunk cap.**
File-scope locals there are scarce - the file has hit the wall before (Phase 8
S4b + S5, #301). Prefer a lazy `use "X"` at the call site, or pack related
values into one table, before spending a slot. Measure the headroom rather than
trusting a number written down here: `luac -p -l core/hub.lua` prints the main
chunk's `N locals` in its header (plain `-p` only speaks up once you are
already over).

### Plugin / hook model

Expand Down Expand Up @@ -255,20 +272,31 @@ cross-host to a signed CDN URL), boot-time runtime-dir self-heal
plus Sopor-reported fixes (v3.1.13 ratelimit hub-crash #401, `usr_uptime`
undercount #405, BLOM smoke de-flake #408; **v3.1.14** Windows `FD_SETSIZE`
64->1024 hub-crash #416, Sopor - the Windows luasocket build inherited the
Winsock-default 64-socket `select()` cap, `>1024` needs the `select`->`poll`
port #310). Shipped to master 2026-07-13 (#419/#420/#423 all closed): the
Winsock-default 64-socket `select()` cap; the Linux `>1024` sibling of that
crash was the `select`->`poll` port, done in #310/#436). Shipped to master
2026-07-13 (#419/#420/#423 all closed): the
ADC parser now discards messages with unknown escape sequences per ADC 3.1
(#419) + hub-bot INF `EM` escaping (#423), and an `etc_webhook` body-field
`conditions` filter (#420 - fixed a live double-announce by filtering on a
JSON body field like a GitHub release `action=released` or a Discourse
opening post, not just the event header). On `dev` (PR #432):
`core/sysinfo.lua` falls back `Get-CimInstance` -> `Get-WmiObject` so
old-Windows hubowners (Server 2008 R2 / Win7 = PowerShell 2.0, which has
no `Get-CimInstance`) get real `+hubinfo` OS/CPU/RAM instead of
opening post, not just the event header).

**On `dev`, pending testhub validation then a dev->master MERGE** (exact
delta: `git log --oneline origin/master..origin/dev` - do not trust a list
written here): the `select`->`poll` event-loop port (#310 / PR #436) which
removes the ~1024 concurrent-socket ceiling on POSIX and leaves Windows on
`select`; `core/sysinfo.lua`'s `Get-CimInstance` -> `Get-WmiObject` fallback
(#432) so old-Windows hubowners (Server 2008 R2 / Win7 = PowerShell 2.0,
which has no `Get-CimInstance`) get real `+hubinfo` OS/CPU/RAM instead of
`<UNKNOWN>` (Sopor; the pre-refactor 3.1.x plugin also CRASHED on the
nil-concat - shipped a v0.30 `cmd_hubinfo` drop-in per §8). Many
hubowners run ancient Windows (the UCRT release build also needs
KB2999226 there - the Universal C Runtime). A
nil-concat - shipped a v0.30 `cmd_hubinfo` drop-in per §8); a `release.yml`
zlib-dev fix (#441 - `find_package(ZLIB REQUIRED)` is unconditional but no
release leg installed the dev package, so the first `v3.2.0` tag would have
failed the aarch64 build at configure); a repo-wide plugin lang-key guard
(#442); `CONTRIBUTING.md` documenting the dev-branch policy (#440); and
three external cleanups from @Kcchouette (#437/#438/#439). Many hubowners
run ancient Windows (the UCRT release build also needs KB2999226 there -
the Universal C Runtime). A
recurring pattern this era:
a **periodic-fetch plugin must persist its next-fetch deadline across
`+reload`**, or every reload re-hits a rate-limited provider - fixed twice
Expand Down
21 changes: 18 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,24 @@ message(STATUS "luadch: OpenSSL ${OPENSSL_VERSION} at ${OPENSSL_INCLUDE_DIR}")
# -----------------------------------------------------------------------------
# zlib: Phase 8 S4b (ADC-EXT ZLIF stream compression). Used by the
# zlib_stream C module that wraps deflate / inflate streams with the
# Z_SYNC_FLUSH semantic ADC mandates. System zlib on Linux + MinGW
# (Ubuntu CI installs zlib1g-dev). aarch64 Bullseye container has zlib
# as a base-system package - no change needed.
# Z_SYNC_FLUSH semantic ADC mandates. This is the one dependency that is
# NOT bundled - it must come from the build environment.
#
# EVERY build path must install the zlib DEVELOPMENT package explicitly;
# there is no environment where it can be assumed present. The runtime
# library (libz.so.1) is on virtually every Linux box because half of
# userspace links it, but the headers and the libz.so link ship only in
# zlib1g-dev, and build-essential does NOT depend on it - so this
# find_package fails on an otherwise complete toolchain. An earlier
# version of this comment claimed the aarch64 Bullseye container had zlib
# "as a base-system package - no change needed"; that was wrong and left
# all three release.yml legs without it (never noticed, because the
# release workflow only fires on v* tags and the 3.1.x line predates
# zlib_stream). Install sites, keep in sync:
# .github/workflows/release.yml - zlib1g-dev (x2), mingw-w64-ucrt-x86_64-zlib
# .github/workflows/smoke.yml - zlib1g-dev, mingw-w64-ucrt-x86_64-zlib
# docker/Dockerfile - zlib-dev (alpine)
# Building from source? See docs/BUILDING.md.
# -----------------------------------------------------------------------------
find_package(ZLIB REQUIRED)
message(STATUS "luadch: ZLIB ${ZLIB_VERSION_STRING} at ${ZLIB_INCLUDE_DIRS}")
Expand Down
70 changes: 70 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Contributing to luadch

Thanks for wanting to contribute. This page covers the things that are easy to
get wrong from the outside. The engineering how-to (core modules, plugins,
testing, security checklists, Definition of Done) lives in
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).

## Open pull requests against `dev`, not `master`

GitHub offers `master` by default because it is the repository's default
branch - but `master` is the release substrate and only receives changes that
have already been validated. New work goes to `dev` first.

| Branch | What it is | Target it? |
|---|---|---|
| **`dev`** | Staging. Everything lands here first and is validated on a test hub (`ghcr.io/luadch-ng/luadch:dev` is rebuilt on every push). | **Yes - this one** |
| `master` | Release substrate for the 3.2.x line. Promoted from `dev` once validated; release tags are cut here. | No (maintainers) |
| `release/3.1.x` | Maintenance line, critical security backports only. | No (maintainers) |

If you already opened a PR against `master`, nothing is lost - a maintainer
can retarget it to `dev` in one click, and it stays your PR with your
authorship intact. No need to close and reopen it.

**Found a security vulnerability?** Do not open a public issue or PR - see
[`docs/SECURITY.md`](docs/SECURITY.md) for the reporting process.

## Before opening the PR

- **Build it.** [`docs/BUILDING.md`](docs/BUILDING.md) has the Linux, Windows
and ARM recipes.
- **Run the tests.** [`tests/README.md`](tests/README.md) - a Lua unit suite
plus a protocol-level smoke harness. Both run in CI on Linux *and* Windows
for every PR, so running them locally first saves a round trip.
- **One logical change per PR**, referencing the issue it addresses. Unrelated
fixes belong in separate PRs - it keeps review honest and makes reverts
surgical.
- **Match the surrounding code.** Lua style follows the file you are editing.
Comments explain *why*, not *what*. Avoid drive-by refactors: if you spot
something unrelated, open an issue instead.
- **Bug fixes should come with a test** that fails before the fix and passes
after. The exception is a diff that is self-evidently its own proof (a typo,
dead code, a redundant call). See
[`docs/DEVELOPMENT.md` §4](docs/DEVELOPMENT.md).

## Where things live

- **`core/*.lua`** - the hub itself. It runs in a restricted environment where
every global must be imported (`local X = use "X"`); a bare global passes
unit tests but fails at hub boot. Read
[`docs/DEVELOPMENT.md` §2](docs/DEVELOPMENT.md) before touching core.
- **`scripts/*.lua`** - bundled plugins, running in a sandbox.
[`docs/PLUGIN_API.md`](docs/PLUGIN_API.md) is the API reference;
[`docs/DEVELOPMENT.md` §3](docs/DEVELOPMENT.md) adds the conventions on top
of it.
- **Additional plugins** that do not ship with the hub live in the companion
repo [`luadch-ng/scripts`](https://github.com/luadch-ng/scripts).
- **`tests/`** - unit tests (`tests/unit/`) and the smoke harness
(`tests/smoke/run.py`).

## Reporting bugs

Open an issue with the hub version (`+hubinfo` output or the boot line in
`log/event.log`), the platform, and what you did to trigger it. Log excerpts
from `log/error.log` help. If the report is about an older release, say which
one - a good share of reports turn out to be already fixed on the current
line.

## License

Contributions are made under the project's [GPL-3.0](LICENSE) license.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ The 3.1.x line concludes the modernisation programme (Phases 1-7 +
ADC-coverage closure). The active 3.2.x line picks up the Phase 8
feature work.

**Contributing?** New work is staged on the `dev` branch before it is
promoted to `master`, so pull requests go against **`dev`** - see
[CONTRIBUTING.md](CONTRIBUTING.md).

## Original Features

- TLS 1.3 with AES-128 / AES-256 cipher suites
Expand Down Expand Up @@ -65,6 +69,11 @@ and operator guidance.
TLS-only deployments, troubleshooting
- **[docs/PLUGIN_API.md](docs/PLUGIN_API.md)** - plugin scripting API
reference (listeners, modules, objects, conventions, pitfalls)
- **[CONTRIBUTING.md](CONTRIBUTING.md)** - which branch to target, what a
PR needs, how to report a bug
- **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)** - engineering how-to:
authoring core modules, plugin conventions, testing, security
checklists, Definition of Done

## Quick start

Expand Down
15 changes: 3 additions & 12 deletions core/adc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ local tostring = use "tostring"
--// lua libs //--

local os = use "os"
local math = use "math"
local table = use "table"
local debug = use "debug"
local string = use "string"

--// lua lib methods //--
Expand All @@ -56,10 +54,8 @@ local string_sub = string.sub
local string_gsub = string.gsub
local string_find = string.find
local string_match = string.match
local table_insert = table.insert
local table_concat = table.concat
local table_remove = table.remove
local debug_traceback = debug.traceback

--// extern libs //--

Expand All @@ -79,7 +75,6 @@ local string_byte = string.byte
--// core scripts //--

local out = use "out"
local mem = use "mem"
local types = use "types"

--// core methods //--
Expand Down Expand Up @@ -140,7 +135,6 @@ local _sup
local _sta
local _bool
local _onetwo
local _integer
local _feature

local _contextsend
Expand All @@ -159,7 +153,6 @@ _sid = "^" .. string.rep( "[A-Z2-7]", 4 ) .. "$"
_sup = "^" .. string.rep( "[A-Z]", 3 ) .. "[A-Z0-9]$"
_bool = "^[1]?$"
_onetwo = "^[12]?$"
_integer = "^%d*$"
_feature = "[%+%-][A-Z]" .. string.rep( "[A-Z0-9]", 3 )

_regex = {
Expand All @@ -170,9 +163,6 @@ _regex = {
sid = function( str )
return string_match( str, _sid )
end,
bool = function( str )
return string_match( str, _sid )
end,
bool = function( str )
return string_match( str, _bool )
end,
Expand Down Expand Up @@ -353,8 +343,9 @@ _protocol = {
FM = _regex.default,
FB = _regex.default,
--// ASCH - Extended searching capability //-- http://adc.sourceforge.net/ADC-EXT.html#_asch_extended_searching_capability
FC = _regex.default,
TO = _regex.default,
-- ASCH also uses FC and TO; both are already declared
-- above in this same STA np table, so they are not
-- repeated here.
RC = _regex.default,


Expand Down
Loading
Loading