1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
# 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).
- **`sock::read_http` malformed `Content-Length:` crash (a3908e1-class):**
`read_http` matched the header on the 15-char prefix `Content-Length:`
(no space required) but then assumed the canonical `Content-Length:
<value>` form and `substr`'d from index 16. Two reachable crash cases
leaked through for any unauthenticated client: a bare `Content-Length:`
(15 chars) made `substr(16, len-16)` throw `std::out_of_range` (pos > size)
> uncaught > process crash; `Content-Length:\n` (16 chars, no value) left
the substring empty so the `do/while` digit scan read past the buffer (OOB
read) until a stray `\n` in adjacent memory. Now guarded: require the
space separator + a value before `substr`, and bound the scan to the
substring length. (ychat's a3908e1 fixed the same malformed-input-crash
class in its own structurally different request parser.)
- **`sock::start` unchecked `accept`:** the `accept()` return was used
unchecked; on failure (`fd == -1`, e.g. EMFILE/ENFILE under fd exhaustion,
EINTR) `FD_SET(-1, &active_fd_set)` is UB (bit-op on a negative index) and
the later `_create_container(-1)` would read/write fd -1 (EBADF). Now bails
with `ACCPERR` and `continue`s on any `accept` error (the accept-bail half
of ychat's 1c36abe, which the original yhttpd port only carried the
`size_t`→`socklen_t` init of).
- **`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.
|