summaryrefslogtreecommitdiff
path: root/yhttpd
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
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')
-rw-r--r--yhttpd/.dockerignore8
-rw-r--r--yhttpd/.gitignore12
-rw-r--r--yhttpd/DOCKER.md126
-rw-r--r--yhttpd/Dockerfile56
-rw-r--r--yhttpd/Makefile6
-rwxr-xr-xyhttpd/configure26
-rw-r--r--yhttpd/html/index.html2
-rw-r--r--yhttpd/log/.gitkeep0
-rw-r--r--yhttpd/src/conf/conf.cpp2
-rwxr-xr-xyhttpd/src/configure14
-rw-r--r--yhttpd/src/glob.h18
-rw-r--r--yhttpd/src/html.cpp2
-rw-r--r--yhttpd/src/logd.cpp15
-rw-r--r--yhttpd/src/main.cpp2
-rw-r--r--yhttpd/src/msgs.h8
-rw-r--r--yhttpd/src/reqp.cpp9
-rw-r--r--yhttpd/src/sock/sock.cpp55
-rw-r--r--yhttpd/src/tool/tool.cpp124
18 files changed, 396 insertions, 89 deletions
diff --git a/yhttpd/.dockerignore b/yhttpd/.dockerignore
new file mode 100644
index 0000000..969b9fc
--- /dev/null
+++ b/yhttpd/.dockerignore
@@ -0,0 +1,8 @@
+obj
+bin
+backuped
+log
+*.version
+src/Makefile
+src/includes.add
+src/libs.add
diff --git a/yhttpd/.gitignore b/yhttpd/.gitignore
new file mode 100644
index 0000000..e9adcbb
--- /dev/null
+++ b/yhttpd/.gitignore
@@ -0,0 +1,12 @@
+bin
+obj/
+g++.version
+make.version
+src/Makefile
+src/includes.add
+src/libs.add
+backuped/
+log/*
+!log/.gitkeep
+*.o
+*.so
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
diff --git a/yhttpd/Dockerfile b/yhttpd/Dockerfile
new file mode 100644
index 0000000..515932f
--- /dev/null
+++ b/yhttpd/Dockerfile
@@ -0,0 +1,56 @@
+# yhttpd — Docker build (revival)
+#
+# yhttpd is a minimal httpd derived from ychat's socket/engine. This image
+# builds it entirely in a container on Rocky Linux 9 (its bespoke perl build
+# system was gated to g++ 3.x; the configure now accepts any GNU g++).
+#
+# NCURSES and CLI are disabled (glob.h) so the build needs no ncurses and
+# produces a pure httpd. The server listens on port 2000 (httpd.serverport).
+#
+# STATUS: with the ported fixes yhttpd now BUILDS on a modern toolchain and
+# serves requests without crashing when built+run here (Rocky Linux 9 /
+# GCC 11). A SIGILL heap-corruption crash in sock::_close DOES reproduce
+# when built on a newer host toolchain (GCC 16) - see yhttpd/DOCKER.md for
+# why this doesn't mean it's fixed. Concurrent bursts still see some
+# connection resets (not crashes). See yhttpd/DOCKER.md for details.
+
+# ---------- builder ----------
+FROM rockylinux:9 AS builder
+
+RUN dnf -y install \
+ gcc-c++ \
+ make \
+ perl \
+ which \
+ libevent-devel \
+ && dnf clean all
+
+WORKDIR /build/yhttpd
+COPY . .
+RUN rm -rf obj bin backuped g++.version make.version src/Makefile src/includes.add src/libs.add
+
+# config.pl is interactive (asks yes/no); "yes" = use default before-compile
+# options (keeps our glob.h: NCURSES/CLI disabled). src/configure scans glob.h
+# and drops the ncur/ + cli/ sources from the build accordingly.
+RUN echo yes | ./configure \
+ && make
+
+# ---------- runtime ----------
+FROM rockylinux:9 AS runtime
+
+RUN dnf -y install libevent libstdc++ tzdata ca-certificates \
+ && dnf clean all
+
+RUN useradd -r -u 1000 -d /app -s /sbin/nologin yhttpd
+
+WORKDIR /app
+COPY --from=builder /build/yhttpd/bin /app/bin/yhttpd
+COPY --from=builder /build/yhttpd/etc/ /app/etc/
+COPY --from=builder /build/yhttpd/html/ /app/html/
+
+RUN mkdir -p /app/log && chown -R yhttpd:yhttpd /app
+
+USER 1000:1000
+EXPOSE 2000
+
+ENTRYPOINT ["/app/bin/yhttpd"] \ No newline at end of file
diff --git a/yhttpd/Makefile b/yhttpd/Makefile
index a33780f..a5c4635 100644
--- a/yhttpd/Makefile
+++ b/yhttpd/Makefile
@@ -38,13 +38,13 @@ clean_modules:
@if test -d ./src/mods; then ${MAKE} -C ./src/mods clean; fi
base:
@if test -f bin/yhttpd; then echo "Backing up old binary";if test -f bin/yhttpd.old; then rm -f bin/yhttpd.old; fi; mv bin/yhttpd bin/yhttpd.old; fi
- @perl ./scripts/buildnr.pl
- @perl ./scripts/setglobvals.pl
+ @perl -I. ./scripts/buildnr.pl
+ @perl -I. ./scripts/setglobvals.pl
@${MAKE} -C ./src
clean_base:
@${MAKE} -C ./src clean
stats:
- @perl scripts/stats.pl
+ @perl -I. scripts/stats.pl
run:
./bin/yhttpd
base_start: base
diff --git a/yhttpd/configure b/yhttpd/configure
index 9aab971..56f015f 100755
--- a/yhttpd/configure
+++ b/yhttpd/configure
@@ -51,17 +51,25 @@ perl -e '
}
&check_make;
print "Checking compiler version\n";
- my $r = 0;
- for (my $i = 4; $i > 0 && $r == 0; --$i ) {
- $r = &check_gcc(3,$i);
- }
- if ($r == 0) {
- print "No suitable g++ compiler found!\n";
- print "Please install a right version of GNU G++!\n";
+ # Accept any GNU g++ (the original check only accepted g++ 3.x; modern GCC
+ # works fine with the legacy-C++ fixes applied). Write g++.version as
+ # "<version>\ng++\n" - src/configure reads the last line as the compiler.
+ my $ver = `g++ -dumpversion 2>/dev/null`;
+ chomp $ver;
+ if ( $ver eq "" ) {
+ print "No GNU g++ compiler found!\nPlease install GNU G++!\n";
exit(1);
}
-
- system("perl \"scripts/config.pl\"");
+ `echo $ver > g++.version`;
+ `echo g++ >> g++.version`;
+ print "GNU G++ $ver found!\n";
+
+ # -I. : scripts/config.pl does "use scripts::modules::file", which needs
+ # "." on @INC. Perl 5.26+ dropped "." from the default @INC, so this
+ # silently BEGIN-failed on modern Perl (the "yes" answer was never even
+ # read - it happened to be harmless only because the committed glob.h
+ # defaults were already correct).
+ system("perl -I. \"scripts/config.pl\"");
chdir("src");
system("./configure ".join(" ", @ARGV));
chdir("..");
diff --git a/yhttpd/html/index.html b/yhttpd/html/index.html
index 66dca39..6ea5b6d 100644
--- a/yhttpd/html/index.html
+++ b/yhttpd/html/index.html
@@ -22,6 +22,6 @@ This is the demo website provided by yhttpd :-)
</tr>
</table>
<br><br>
-<center><span class="signature">yhttpd is OpenSource - get it at <a class="fancy" target="_blank" href="http://www.yChat.org">http://www.yChat.org</a></span></center>
+<center><span class="signature">yhttpd is OpenSource - get it at <a class="fancy" target="_blank" href="https://codeberg.org/snonux/ychat">https://codeberg.org/snonux/ychat</a></span></center>
</body>
</html>
diff --git a/yhttpd/log/.gitkeep b/yhttpd/log/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/yhttpd/log/.gitkeep
diff --git a/yhttpd/src/conf/conf.cpp b/yhttpd/src/conf/conf.cpp
index 25884ca..f987755 100644
--- a/yhttpd/src/conf/conf.cpp
+++ b/yhttpd/src/conf/conf.cpp
@@ -174,7 +174,7 @@ conf::get_vector(string s_key)
vector<string> vec_ret;
string s_val = get_elem(s_key);
- for (unsigned i_pos = s_val.find(" "); i_pos != string::npos; i_pos = s_val.find(" "))
+ for (size_t i_pos = s_val.find(" "); i_pos != string::npos; i_pos = s_val.find(" "))
{
vec_ret.push_back(s_val.substr(0, i_pos));
s_val = s_val.substr(i_pos+1);
diff --git a/yhttpd/src/configure b/yhttpd/src/configure
index 54cf650..b67da46 100755
--- a/yhttpd/src/configure
+++ b/yhttpd/src/configure
@@ -55,8 +55,11 @@ perl -e '
$ENV{HOME}."/lib",
$ENV{HOME}."/usr/lib",
"/lib",
+ "/lib64",
"/usr/lib",
+ "/usr/lib64",
"/usr/local/lib",
+ "/usr/local/lib64",
"/usr/pkg/lib",
"/opt/lib",
"/opt/local/lib"
@@ -65,6 +68,7 @@ perl -e '
my %dependfiles = (
database => ["data"],
ycurses => ["curses", "ycui.cpp", "ycui.h"],
+ ncur => ["ncur"],
logging => ["logd.cpp", "logd.h"],
cli => ["cli"],
opnssl => ["sock/sslsock.cpp", "sock/sslsock.h"]
@@ -101,6 +105,16 @@ perl -e '
`mv ../backuped/$_ .` for @{$dependfiles{cli}};
}
+ if ( /\/\/#define NCURSES/ ) {
+ remove_from_array("ncurses.h",\@headers);
+ remove_from_array("libncurses.so",\@libs);
+ mkdir "../backuped" unless -d "../backuped";
+ `mv $_ ../backuped` for @{$dependfiles{ncur}};
+ }
+ elsif ( /^#define NCURSES/ && !-d "ncur") {
+ `mv ../backuped/$_ .` for @{$dependfiles{ncur}};
+ }
+
if ( /\/\/#define YCURSES/ ) {
for ("ncurses", "menu", "panel") {
remove_from_array("$_.h",\@headers);
diff --git a/yhttpd/src/glob.h b/yhttpd/src/glob.h
index a2c1334..2283f1e 100644
--- a/yhttpd/src/glob.h
+++ b/yhttpd/src/glob.h
@@ -24,7 +24,7 @@
/* - CONFIG -
Should yhttpd get compiled with comand line interface support?
*/
-#define CLI
+//#define CLI
/* - CONFIG -
What should be the name of the config file?
@@ -70,7 +70,7 @@
/* - CONFIG -
Should yhttpd get compiled with ncurses support?
*/
-#define NCURSES
+//#define NCURSES
/* - CONFIG -
Please specify the maximum length of a HTTP post request.
@@ -84,12 +84,20 @@
#define READBUF 2048
/* - CONFIG -
- Please specify the maximum length of a line read from a socket
+ Please specify the maximum length of a line read from a socket
or a file. ( config-file, html-template )
*/
#define READSOCK 2048
/* - CONFIG -
+ Upper bound (bytes) on how much unread request data sock::_close() will
+ drain from a socket before giving up and closing anyway. Bounds the
+ non-blocking drain loop against a client that keeps streaming data after
+ we've half-closed the connection.
+*/
+#define DRAINMAX 8192
+
+/* - CONFIG -
In which prefix should yhttpd be installed if typing gmake inst-
all?
*/
@@ -175,7 +183,7 @@
using namespace std;
-typedef int function( void *v_arg );
+typedef int mod_func_t( void *v_arg );
struct container
{
@@ -184,7 +192,7 @@ struct container
struct dynmod
{
- function *the_func ;
+ mod_func_t *the_func ;
void *the_module;
};
diff --git a/yhttpd/src/html.cpp b/yhttpd/src/html.cpp
index ace041a..5794612 100644
--- a/yhttpd/src/html.cpp
+++ b/yhttpd/src/html.cpp
@@ -78,7 +78,7 @@ html::parse( map<string,string> &map_params )
}
// find %%KEY%% token and substituate those.
- unsigned pos[2];
+ size_t pos[2];
pos[0] = pos[1] = 0;
for(;;)
diff --git a/yhttpd/src/logd.cpp b/yhttpd/src/logd.cpp
index 664cd14..d65c9bd 100644
--- a/yhttpd/src/logd.cpp
+++ b/yhttpd/src/logd.cpp
@@ -7,6 +7,9 @@
#ifdef LOGGING
#include <fstream>
+#include <iostream>
+#include <cstring>
+#include <cerrno>
logd::logd( string s_filename, string s_log_lines )
{
@@ -62,9 +65,13 @@ logd::flush()
ofstream of_output;
of_output.open(s_logfile.c_str(), ios::app);
- if( of_output == NULL )
+ if ( ! of_output.is_open() )
{
- wrap::system_message( LOGERR1 + s_logfile );
+ // Log to stderr only; do NOT call wrap::system_message() - that routes
+ // back through LOGD->log_simple_line -> flush on the same failing logd
+ // and would recurse infinitely (stack overflow).
+ cerr << "yhttpd: could not open logfile '" << s_logfile << "' ("
+ << strerror(errno) << ")" << endl;
exit(1);
}
@@ -118,7 +125,7 @@ void
logd::set_logfile( string s_path, string s_filename )
{
// Remove "/" from filename!
- unsigned i_pos = s_filename.find( "/" );
+ size_t i_pos = s_filename.find( "/" );
while ( i_pos != string::npos )
{
s_filename.replace( i_pos, 1, "SLASH" );
@@ -149,7 +156,7 @@ logd::flush_logs()
string
logd::remove_html_tags( string s_logs )
{
- unsigned pos[2];
+ size_t pos[2];
while ( (pos[0] = s_logs.find("<")) != string::npos )
{
diff --git a/yhttpd/src/main.cpp b/yhttpd/src/main.cpp
index 87f1cab..b18f6cc 100644
--- a/yhttpd/src/main.cpp
+++ b/yhttpd/src/main.cpp
@@ -1,5 +1,5 @@
/*
- * yhttpd; Contact: www.yChat.org; Mail@yChat.org
+ * yhttpd; Contact: https://codeberg.org/snonux/ychat; Mail@yChat.org
* Copyright (C) 2003 Paul C. Buetow, Volker Richter
* Copyright (C) 2004 Paul C. Buetow
* Copyright (C) 2005 EXA Digital Solutions GbR
diff --git a/yhttpd/src/msgs.h b/yhttpd/src/msgs.h
index fdfb68c..bedf4ec 100644
--- a/yhttpd/src/msgs.h
+++ b/yhttpd/src/msgs.h
@@ -9,7 +9,7 @@
#define HTTPDFLO "Chat: Flooding ("
#define CFILEOK "Parsing config file"
#define CFILEFA "Failed opening config file!"
-#define CONTACT "Contact: http://www.yChat.org, Mail@yChat.org, ICQ: 11655527"
+#define CONTACT "Contact: https://codeberg.org/snonux/ychat, Mail@yChat.org, ICQ: 11655527"
#define CLRHTML "HTML: Cleared the document cache "
#define CLIWELC "Command Line Interface (type help for a list of all commands)"
#define CLIPRMO ">> "
@@ -125,9 +125,9 @@
#define XMLER1 "XML Error: Unable to load file "
#define VERSION "0.8"
#define BRANCH "CURRENT"
-#define BUILDNR 4003
-#define UNAME "FreeBSD 5.4-DEVEL-p3 i386"
-#define COMPOPT "Using built-in specs.; Configured with: FreeBSD/i386 system compiler; Thread model: posix; gcc version 3.4 [FreeBSD] 20040728; 3.4; g++"
+#define BUILDNR 4027
+#define UNAME "Linux 7.0.11-200.fc44.x86_64 x86_64"
+#define COMPOPT "16; g++"
#define YCUSAGE "Usage: ./yhttpd {h|v}|{o confkey confvalue}\n"
#define HEADER1 "HTTP/1.1 200 OK\r\n"
diff --git a/yhttpd/src/reqp.cpp b/yhttpd/src/reqp.cpp
index df9de5d..4ce30d9 100644
--- a/yhttpd/src/reqp.cpp
+++ b/yhttpd/src/reqp.cpp
@@ -22,7 +22,7 @@ void
reqp::get_request_parameters( string s_parameters, map<string,string>& map_params )
{
string s_tmp;
- unsigned i_pos, i_pos2;
+ size_t i_pos, i_pos2;
while( (i_pos = s_parameters.find("&")) != string::npos )
{
@@ -46,7 +46,7 @@ reqp::get_request_parameters( string s_parameters, map<string,string>& map_param
string
reqp::get_url( string s_req, map<string, string> &map_params, int& i_postpayloadoffset )
{
- unsigned i_pos, i_pos2;
+ size_t i_pos, i_pos2;
string s_vars( "" );
string s_ret;
int i_req;
@@ -111,7 +111,6 @@ reqp::get_url( string s_req, map<string, string> &map_params, int& i_postpayload
if ( s_ret.empty() )
s_ret = wrap::CONF->get_elem( "httpd.startsite" );
-
else
s_ret = remove_dots(s_ret);
@@ -211,7 +210,7 @@ reqp::url_decode( string s_url )
string
reqp::get_from_header( string s_req, string s_hdr )
{
- unsigned i_pos[2];
+ size_t i_pos[2];
if ( (i_pos[0] = s_req.find( s_hdr, 0 )) == string::npos )
return "";
@@ -274,7 +273,7 @@ string
reqp::remove_dots( string s_ret )
{
// remove ".." from the request.
- unsigned i_pos;
+ size_t i_pos;
if ( (i_pos = s_ret.find( ".." )) != string::npos )
return remove_dots(s_ret.substr(0, i_pos));
diff --git a/yhttpd/src/sock/sock.cpp b/yhttpd/src/sock/sock.cpp
index 5ae0be1..279123d 100644
--- a/yhttpd/src/sock/sock.cpp
+++ b/yhttpd/src/sock/sock.cpp
@@ -3,6 +3,7 @@
#include <arpa/inet.h>
#include <errno.h>
+#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
@@ -40,8 +41,39 @@ sock::_read(socketcontainer *p_sock, char *sz, int len)
int
sock::_close(socketcontainer *p_sock)
{
- shutdown( p_sock->i_sock, 2 );
- close ( p_sock->i_sock );
+ int i_sock = p_sock->i_sock;
+
+ // read_http() only ever consumes the GET request line (POST bodies are
+ // fully read, but GET headers after the first line are intentionally
+ // skipped) - so the kernel receive buffer usually still has unread
+ // bytes at this point. Closing a socket with unread data makes Linux
+ // send an abortive RST instead of a graceful FIN, which can race the
+ // client's read of the response we just sent and show up as
+ // "connection reset by peer" even though the response was delivered.
+ // The client already sent its whole request before we ever got here
+ // (we already read the first line via blocking reads), so any unread
+ // header bytes are already sitting in the receive buffer - a
+ // non-blocking drain picks them up with no added latency, unlike a
+ // timed wait for a client-initiated close that (for ordinary HTTP
+ // clients waiting on the response) would rarely arrive in time anyway.
+ shutdown( i_sock, SHUT_WR );
+
+ // Skip the drain (fall straight through to close()) if we can't make the
+ // socket non-blocking - reading below would otherwise risk blocking this
+ // worker thread on a client that never sends more data.
+ int i_flags = fcntl( i_sock, F_GETFL, 0 );
+ if ( i_flags != -1 && fcntl( i_sock, F_SETFL, i_flags | O_NONBLOCK ) != -1 )
+ {
+ // Bounded: a client that keeps streaming data after our SHUT_WR could
+ // otherwise keep this non-blocking loop spinning for as long as it
+ // keeps pushing bytes.
+ char c_drain[256];
+ int i_drained = 0;
+ while ( i_drained < DRAINMAX && read( i_sock, c_drain, sizeof(c_drain) ) > 0 )
+ i_drained += sizeof(c_drain);
+ }
+
+ close( i_sock );
delete p_sock;
}
@@ -292,7 +324,7 @@ sock::read_write(socketcontainer* p_sock)
// get the s_rep ( s_html response which will be send imediatly to the client
struct sockaddr_in client;
- size_t size = sizeof(client);
+ socklen_t size = sizeof(client);
getpeername(i_sock, (struct sockaddr *)&client, &size);
@@ -353,7 +385,6 @@ sock::start()
{
wrap::system_message( SOCKSRV );
pool* p_pool = wrap::POOL;
- int i_sock = i_server_sock;
#ifdef NCURSES
@@ -361,15 +392,25 @@ sock::start()
p_pool->print_pool_size();
#endif
- int i_port = tool::string2int( wrap::CONF->get_elem( "httpd.serverport" ) );
+ // i_server_sock is already a bound listening-socket fd here:
+ // wrap::init_wrapper() (wrap.cpp) calls _make_server_socket() before
+ // start() ever runs. Do not call it again - besides leaking this fd, a
+ // second call goes through sslsock's virtual override too (if OPENSSL
+ // is ever enabled), which would leak the first SSL_CTX and re-read the
+ // cert/key files.
+ int i_sock = i_server_sock;
_main_loop_init();
int i;
fd_set active_fd_set, read_fd_set;
struct sockaddr_in clientname;
- size_t size;
+ socklen_t size;
- if (listen (i_sock, 1) < 0)
+ // Backlog was hardcoded to 1: with select()'s single accept()-per-wakeup
+ // loop below, any connection arriving while one is already queued got a
+ // kernel-level reset (visible as intermittent connection resets under
+ // concurrent load). SOMAXCONN lets the kernel queue a normal burst.
+ if (listen (i_sock, SOMAXCONN) < 0)
{
wrap::system_message( LISTERR );
exit( EXIT_FAILURE );
diff --git a/yhttpd/src/tool/tool.cpp b/yhttpd/src/tool/tool.cpp
index dbb1f22..90314fe 100644
--- a/yhttpd/src/tool/tool.cpp
+++ b/yhttpd/src/tool/tool.cpp
@@ -7,6 +7,7 @@
#include <sys/wait.h>
#include <stdio.h>
#include <sys/types.h>
+#include <sys/stat.h>
#include <fcntl.h>
#include "tool.h"
@@ -104,7 +105,7 @@ list<string>
tool::split_string(string s_string, string s_split)
{
list<string> list_ret;
- unsigned i_pos, i_len = s_split.length();
+ size_t i_pos, i_len = s_split.length();
while ( (i_pos = s_string.find(s_split)) != string::npos )
{
@@ -120,30 +121,19 @@ tool::split_string(string s_string, string s_split)
string
tool::trim( string s_str )
{
- if( s_str.empty() )
- return s_str;
-
- char c_cur = s_str[0];
- int i_pos = 0;
-
- // left trim
- while ( c_cur == ' '|| c_cur == '\n' || c_cur == '\r' )
- {
- s_str.erase(i_pos,1);
- c_cur = s_str[++i_pos];
- }
-
- // right trim
- i_pos = s_str.size();
- c_cur = s_str[s_str.size()];
-
- while ( c_cur == ' ' || c_cur == '\n' || c_cur == '\0' || c_cur == '\r' )
- {
- s_str.erase(i_pos, 1);
- c_cur = s_str[--i_pos];
- }
-
- return s_str;
+ // Left trim: find first non-whitespace char.
+ size_t b = 0;
+ while ( b < s_str.size() &&
+ ( s_str[b] == ' ' || s_str[b] == '\n' || s_str[b] == '\r' ) )
+ ++b;
+
+ // Right trim: find last non-whitespace char.
+ size_t e = s_str.size();
+ while ( e > b &&
+ ( s_str[e-1] == ' ' || s_str[e-1] == '\n' || s_str[e-1] == '\r' ) )
+ --e;
+
+ return s_str.substr( b, e - b );
}
char*
@@ -160,7 +150,7 @@ tool::clean_char( char* c_str )
string
tool::replace( string s_string, string s_search, string s_replace )
{
- unsigned i_pos[2];
+ size_t i_pos[2];
for ( i_pos[0] = s_string.find( s_search );
i_pos[0] != string::npos;
@@ -201,37 +191,75 @@ tool::int2char( int i_int )
string
tool::shell_command( string s_command, method m_method )
{
- FILE *file;
- char buf[READBUF];
- char *c_pos;
- string s_ret = "";
+ // Execute the CGI file directly via fork/execve - NOT through a shell.
+ // The old popen() ran `/bin/sh -c <s_command>`, so any shell metacharacter
+ // in the URL-derived request path was a command-injection / RCE vector
+ // when httpd.enablecgi=true. s_command must be the full path to an
+ // executable file (reqp's remove_dots already prevents ".." escaping the
+ // template dir, and stat() rejects non-files).
+ (void) m_method; // only METH_RETSTRING is used by the CGI path
wrap::system_message(SHELLEX);
wrap::system_message(s_command);
- if( (file=popen(s_command.c_str(), "r")) == NULL )
+ struct stat st;
+ if ( stat(s_command.c_str(), &st) != 0 || ! S_ISREG(st.st_mode) )
+ {
+ wrap::system_message( SHELLER );
+ return "";
+ }
+
+ int fd[2];
+ if ( pipe(fd) != 0 )
+ {
+ wrap::system_message( SHELLER );
+ return "";
+ }
+
+ pid_t pid = fork();
+ if ( pid < 0 )
{
+ close(fd[0]); close(fd[1]);
wrap::system_message( SHELLER );
+ return "";
}
- else
+
+ if ( pid == 0 )
+ {
+ // child: stdout -> pipe, close inherited fds, exec the file (no shell).
+ close(fd[0]);
+ dup2(fd[1], STDOUT_FILENO);
+ close(fd[1]);
+ long l_maxfd = sysconf(_SC_OPEN_MAX);
+ if ( l_maxfd < 0 ) l_maxfd = 256;
+ for ( long i = STDERR_FILENO + 1; i < l_maxfd; ++i )
+ close((int)i);
+ char* argv[] = { const_cast<char*>(s_command.c_str()), (char*) NULL };
+ char* envp[] = { (char*) NULL };
+ execve( s_command.c_str(), argv, envp );
+ _exit(127);
+ }
+
+ // parent: read the CGI output.
+ close(fd[1]);
+ string s_ret;
+ char buf[READBUF];
+ for (;;)
{
- while(true)
- {
- if(fgets(buf, READBUF, file) == NULL)
- break;
-
- switch (m_method)
- {
- case METH_NCURSES:
- wrap::system_message( clean_char(buf) );
- break;
- default:
- s_ret.append("\n" + string(buf));
- } // switch
- }
-
- pclose(file);
+ ssize_t n = read(fd[0], buf, sizeof(buf));
+ if ( n > 0 )
+ s_ret.append( buf, n );
+ else if ( n == 0 )
+ break;
+ else if ( errno == EINTR )
+ continue;
+ else
+ break;
}
+ close(fd[0]);
+
+ int i_status;
+ waitpid( pid, &i_status, 0 );
return s_ret;
}