summaryrefslogtreecommitdiff
path: root/yhttpd/DOCKER.md
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-02 00:53:24 +0300
committerPaul Buetow <paul@buetow.org>2026-07-02 00:53:24 +0300
commit58602a28d2c92b603208f3e01c14c169d28cc7b0 (patch)
tree502d2e187ea29f2cc840e22d299d752d59088a3c /yhttpd/DOCKER.md
parent6c3a65b577f002f3219d03498c66205434765179 (diff)
yhttpd: port ychat fixes, get it building + stable in Docker (task 9s0)
Ports the ychat revival fixes (unsigned/size_t npos truncation, ofstream == NULL, tool::trim OOB, CGI popen -> execve, www.yChat.org links, g++ version gate, config.pl -I., NCURSES/CLI-disabled build) so yhttpd builds on modern GCC in a Rocky Linux 9 container, plus yhttpd-specific fixes found while verifying it under concurrent load: - listen() backlog was hardcoded to 1; bumped to SOMAXCONN. - sock::_close() closed sockets with unread request bytes still in the kernel receive buffer (read_http() only reads the GET line), so Linux sent an abortive RST instead of a FIN, racing the client's read of the response ("connection reset by peer" even though it was delivered). Fixed with a non-blocking, bounded drain before close() - confirmed via tcpdump: RSTs on every response before, zero after, across 140+ requests / concurrent bursts of 20. - Removed a duplicate _make_server_socket() call in start() (wrap.cpp's init_wrapper() already makes it before start() runs) that leaked a fd and would have double-initialized SSL if OPENSSL is ever enabled; caught by fresh-context review, documented honestly in DOCKER.md. - src/configure's dependency-checker predates 64-bit multilib distros (only checked /usr/lib, never /usr/lib64) and was missing an ncur move-aside entry for the NCURSES-disabled build. Added Dockerfile/.dockerignore/DOCKER.md documenting the build, the fixes, and the one known-but-unfixed landmine (a SIGILL heap corruption in sock::_close that reproduces on newer host GCC/glibc but not in the container - latent, not fixed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'yhttpd/DOCKER.md')
-rw-r--r--yhttpd/DOCKER.md126
1 files changed, 126 insertions, 0 deletions
diff --git a/yhttpd/DOCKER.md b/yhttpd/DOCKER.md
new file mode 100644
index 0000000..bba0284
--- /dev/null
+++ b/yhttpd/DOCKER.md
@@ -0,0 +1,126 @@
+# yhttpd — Docker revival (in progress)
+
+`yhttpd` is a minimal standalone http server derived from ychat's
+socket/threading engine (originally ~2003–2005). It is a *subset* of ychat —
+the `yhttpd` tree was generated from the ychat source via
+`scripts/makeyhttpd.pl`.
+
+This revival ports the relevant fixes from `../ychat` and gets yhttpd building
+on a modern toolchain. **It builds in Docker (Rocky Linux 9 / GCC 11) and
+serves sequential and concurrent requests reliably** (verified: 100/100
+requests across 5 bursts of 20 concurrent, 0 TCP resets) — see *Status*
+below for the one remaining known issue on newer host toolchains.
+
+## Build & run (local)
+
+```
+podman build -t yhttpd:dev .
+podman run --rm -p 2000:2000 yhttpd:dev
+curl http://localhost:2000/ # first request returns 200
+```
+
+The server listens on port **2000** (`httpd.serverport` in `etc/yhttpd.conf`).
+Static pages live in `html/`; logs in `/app/log/`.
+
+## What was fixed (ported from ychat + yhttpd-specific)
+
+yhttpd shares ychat's buggy files, so most ychat fixes apply. Ported / fixed:
+
+- **Build (modern g++):** `src/glob.h` renamed the project's `typedef int
+ function(...)` to `mod_func_t` (it collided with `std::function` under
+ `using namespace std;`); `configure`'s g++ 3.x version gate now accepts any
+ GNU g++; `config.pl` is fed `yes` (defaults) non-interactively; `Makefile`
+ perl invocations use `-I.` (modern Perl dropped `.` from `@INC`).
+- **`logd::flush`:** `ofstream == NULL` → `!is_open()`, and a missing logfile
+ is reported to stderr + `exit(1)` instead of recursing through
+ `system_message` → `log_simple_line` → `flush` (stack overflow).
+- **`tool::trim`:** rewrote the right-trim (the old `s_str[s_str.size()]` is
+ out-of-bounds under `_GLIBCXX_ASSERTIONS` / UB).
+- **`tool::shell_command` (CGI):** replaced `popen("/bin/sh -c …")` with
+ `fork/execve` of the file directly (no shell), so URL-derived metacharacters
+ can't inject when `httpd.enablecgi=true` (dormant/off by default).
+- **`tool::replace` and the pervasive `unsigned` string positions:** yhttpd
+ declared many find/substr/replace positions as `unsigned` (32-bit) but
+ `std::string::find` returns `size_t` (64-bit) `npos`. Truncating `npos` to
+ `unsigned` makes `pos == string::npos` *false*, so the wrong branch runs /
+ `substr`/`replace` get `pos=0xFFFFFFFF` → `out_of_range`/infinite recursion.
+ Fixed all such sites (`reqp::get_url`, `reqp::get_request_parameters`,
+ `reqp::parse_headers`' helper, `reqp::remove_dots`, `html::parse`,
+ `conf::get_vector`, `logd::set_logfile`, `logd::remove_html_tags`,
+ `tool::split_string`, `tool::replace`) to `size_t`. (ychat used `int`, where
+ `int(npos)==-1` compares equal to `npos`, so it didn't hit this — yhttpd's
+ `unsigned` did.)
+- **`accept`/`getpeername` `size_t`→`socklen_t`** (modern glibc is strict).
+- **`www.yChat.org` links** → `https://codeberg.org/snonux/ychat`.
+- **NCURSES/CLI disabled** in `glob.h` (no ncurses dep; pure httpd), which
+ needed a `ncur => ["ncur"]` entry added to `src/configure`'s
+ `%dependfiles` (mirroring the existing `cli`/`database`/`ycurses`
+ move-aside-when-disabled handling) so disabling `NCURSES` actually moves
+ `src/ncur/` out of the build tree, matching how the Dockerfile doesn't
+ install `ncurses-devel`.
+
+**Corrected claim (originally in this file):** an earlier pass here claimed
+`sock::_make_server_socket` was "never called" and added a call to it in
+`sock::start()`. That was a misdiagnosis, caught by review: `wrap::
+init_wrapper()` (`wrap.cpp`) already calls it, before `main.cpp` ever calls
+`SOCK->start()`. The added call in `start()` ran a **second**
+`_make_server_socket`, which happened to work (Linux allows a second
+`bind()` to the same wildcard address before either socket calls `listen()`)
+but leaked the first fd and, had `OPENSSL` ever been enabled, would have
+run `sslsock`'s override a second time (leaking the first `SSL_CTX`,
+re-reading the cert/key files). `sock::start()` now just uses the
+already-initialized `i_server_sock` instead of calling
+`_make_server_socket()` again.
+
+## Status — stable, including under concurrent load
+
+With the above, yhttpd **builds on GCC 11** (it didn't before). Note this is
+built and run in Docker (Rocky Linux 9 / GCC 11 / older glibc) — building
+directly on the host (Fedora, GCC 16, newer glibc heap checks) reliably
+crashed with `SIGILL` in `sock::_close` (`delete p_sock`) after the first
+request. That crash does **not** reproduce in the container: 100/100
+requests across 5 bursts of 20 concurrent all returned `200`, and the
+process never restarted or exited. This suggests the corruption is real but
+latent — the older toolchain/glibc in the container just doesn't trip over
+it — rather than fixed. Treat `SIGILL` under `sock::_close` as a known
+landmine if this is ever rebuilt on a newer host toolchain.
+
+Two concurrency bugs were found and fixed while investigating intermittent
+`curl` `000`/"connection reset by peer" failures under concurrent load:
+
+- **`listen()` backlog was hardcoded to `1`.** Bumped to `SOMAXCONN`
+ (`sock.cpp`, `start()`), since the `select()`-based accept loop only calls
+ `accept()` once per event-loop iteration and needs the kernel to hold a
+ real backlog of pending connections in the meantime.
+- **The real cause of the resets: `sock::_close()` closed sockets with
+ unread data still in the kernel receive buffer.** `read_http()` only ever
+ consumes the GET request line and intentionally ignores the rest of the
+ headers the client already sent. Closing a socket like that makes Linux
+ send an abortive RST instead of a graceful FIN — which can race the
+ client's read of the response just sent and surface as "connection reset
+ by peer" even though the response was fully delivered. Fixed by
+ half-closing the write side, then a **non-blocking** drain of whatever's
+ already in the receive buffer before the real `close()` — the client
+ already sent its whole request in one burst before we got here (we'd
+ already read the first line via blocking reads), so the remaining bytes
+ are already buffered and a non-blocking read picks them up with no added
+ latency (a timed/blocking drain was tried first but rejected: ordinary
+ HTTP clients wait on the response instead of half-closing first, so it
+ would pay its full timeout on nearly every request). Confirmed via
+ `tcpdump`: before the fix, RSTs followed every response; after, zero RSTs
+ across 140 requests over multiple concurrent bursts.
+
+Not investigated: the thread pool (`src/thrd/pool.cpp`)'s worker wakeup uses
+`pthread_cond_signal` from `add_task()` without holding the same mutex the
+waiting threads block on (`mut_threads`) — a classic lost-wakeup pattern
+under POSIX. It didn't surface in this testing (concurrency is currently
+bounded by the fix above), but is worth a closer look before relying on
+yhttpd under sustained heavy concurrency.
+
+## Files
+
+- `src/` — sources (NCURSES/CLI sources are still present in the repo; the
+ build's `src/configure` moves `ncur/` + `cli/` aside when those are
+ disabled, at build time).
+- `etc/yhttpd.conf` — config (port, templatedir, logging).
+- `html/` — served static pages. \ No newline at end of file