summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-06-30No-DB build: remove the registration UI completely (build-aware)Paul Buetow
In a no-database (guest-mode) build registration is non-functional (yc_register's body is #ifdef DATABASE), so the password field, the 'Register' link and register.html are misleading. Remove them for the no-DB build while keeping the full login page for a future DB build: - html/index.html: restored to the full login (password field + Register link) - used by a DB build. - html/index_guest.html: new guest login page - no password field, no Register link, a 'guest mode, no password needed' note. - Dockerfile (no-DB build): serve index_guest.html as httpd.startsite, and drop register.html and yc_register.so from the image (the module preload scans the dir, so a missing .so is simply not loaded; an on-demand get_module for the register event fails gracefully -> no-op). Verified: start page is index_guest.html (no password/register); register.html -> 'Page not found'; yc_register.so absent from the image; normal login + multi-user chat still work.
2026-06-30login page: drop the Register link (no-op without a database)Paul Buetow
In Mode A (no DB) the register flow is a no-op: yc_register's body is #ifdef DATABASE, so the registration form at register.html persists nothing and is misleading. Remove the 'Register a new nick' link from index.html, keeping the helpful note that guests log in without a password. (register.html is left in place but no longer linked; the no-DB build has no registration.)
2026-06-30Reclaim oversized-request fds (inline delete) + fix context ctor UAFPaul Buetow
Root cause of the earlier 2s0 oversized-delete crashes: the context constructor initialized p_user=NULL but NOT p_map_params or p_response, so ~context's 'if (p_map_params) delete p_map_params' / 'if (p_response) delete p_response' ran on garbage (non-NULL) pointers on the early-delete paths (EOF-empty, hard read error, oversized) -> a corrupted std::map<string,string> destructor -> infinite _M_erase recursion -> SIGSEGV. It was never a libevent issue. Fix: - src/sock/context.cpp: initialize p_map_params = NULL and p_response = NULL in the constructor (p_user was already NULL). ~context null-guards them, so all early-delete paths are now safe. - src/sock/sock.cpp: the oversized-request branches (headers-incomplete- full and body-incomplete-full) now drop the request inline via del_event()+delete (exactly like the EOF/hard-error sibling branches) instead of leaking the fd/context. This closes the 2s0 fd-exhaustion DoS (the deliberate leak tradeoff) with no crash. Verified: 30 oversized POSTs -> open fd count stays at baseline (7->7, immediate reclaim, no leak), 0 restarts; normal login + multi-user chat work. Independent review: APPROVE (preferred the simplification to inline-delete, applied).
2026-06-30Fix flush_stream hard-error use-after-freePaul Buetow
On a hard write error (EPIPE/ECONNRESET) flush_stream did clear_stream() (sets i_stream_fd = -1) then set_online(false) (adds the user to garbage), while the user's stream context still had its EV_READ|EV_PERSIST event armed on the real, still-open fd and held p_context->p_user. If the hourly gcol::remove_garbage fired before the kernel delivered read EOF, it saw i_stream_fd < 0 (already cleared) and deleted the user - leaving the context's p_user dangling (UAF on the later disconnect). (SIGPIPE is ignored via sign.cpp, so the write returns EPIPE/ECONNRESET safely.) Fix: remove clear_stream() from the hard-error branch (keep i_stream_fd >= 0). remove_garbage's skip-open-stream check (zr0) then keeps the user alive until handle_stream_read (read EOF) reaps the context - the single reaper - after which remove_garbage deletes it safely. set_online(false) also removes the user from its room, so room broadcasts stop targeting the dead fd (no repeated writes). set_online(false)'s b_online guard makes handle_stream_read's later set_online(false) a no-op (no double reap). Verified: abruptly closing a user's stream then posting a room message reaps the user (online list 1->0) with no crash (0 restarts). Independent review: APPROVE.
2026-06-30Fix CGI command injection: execve instead of popen (no shell)Paul Buetow
tool::shell_command (the CGI executor, only called from reqp when httpd.enablecgi=true and the request ends in .cgi) did popen(s_command, "r") = /bin/sh -c <templatedir+request>. The request path is URL- derived, so shell metacharacters (; | $() etc.) in the path were interpreted by the shell -> command injection / RCE the moment CGI is enabled. (Disabled by default; reqp's path-traversal guard (yr0) already prevents '..' escaping the template dir but does not filter metacharacters.) Replace popen with fork/execve of the file directly (no shell): - stat() the path; require a regular file. - pipe + fork; child dup2's stdout to the pipe, closes inherited fds (3..OPEN_MAX) so the CGI can't see/hold the listen socket or other client conns, then execve(path, [path, NULL], [NULL]) with an empty env; _exit(127) on exec failure. - parent reads the pipe to EOF (retrying on EINTR) then waitpid. s_command is passed by value, so the child's COW copy is safe to use post-fork. Verified with httpd.enablecgi=true: a /bin/sh CGI returns its output (CGI-OK); injection attempts sh.cgi;id / test.cgi$(id) / test.cgi|id return empty (no command execution); server stays up. With enablecgi=false (default) test.cgi is served as a static template and normal chat works. Independent review: APPROVE-WITH-NITS; the inherited-fd and EINTR nits were addressed; the remaining nits (empty envp/no RFC3875 vars, no CGI timeout, stat vs lstat) are acceptable for a dormant off-by-default feature and noted for if CGI is ever reactivated.
2026-06-30Fix chat::get_user UB (fell off non-void function on a miss)Paul Buetow
chat::get_user(string&, bool&) returned the user only when found and fell off the end of the non-void function on a miss (undefined behavior; compiler warns 'control reaches end of non-void function'). The 1-arg overload inherits the same UB. All callers check b_found before using the pointer, so add an explicit 'return NULL' on the not-found path to make the return well-defined. (The 1-arg overload appears unused - left in place, now safe via this fix.) Verified: normal chat still works, no crash.
2026-06-30Fix READSOCK=2048 request cap (oversized requests spin/hang)Paul Buetow
src/glob.h: READSOCK 2048 -> 16384 so normal chat requests (incl. reverse proxy X-Forwarded-* headers + a message body) fit. Previously requests >2KB couldn't be fully read; for a POST whose Content-Length exceeded the buffer the completeness check re-armed the read event forever (per- connection spin/hang, no crash). src/sock/sock.cpp handle_client_read: guard both the headers-incomplete and body-incomplete re-arms with (i_buf_len < READSOCK). If the buffer is already full the request can never complete (oversized): drop it without re-arming instead of spinning. Deleting the context inside the read callback corrupts libevent (verified: heap corruption / SIGSEGV, tried both del_event+delete and event_del+delete and a 413-via-write-event), so the oversized request's fd/context is intentionally leaked for that one abusive request rather than crashing the server. The leak is a known limitation (filed as a follow-up: defer reaping via the timer so the fd is reclaimed without deleting inside the read callback). Verified: oversized headers (17KB, no terminator) and oversized POST body (20KB) no longer crash or spin the server (0 restarts); an 8KB POST login now returns 200 (didn't fit in the old 2KB buffer); normal login + multi- user chat still work. Independent review: APPROVE-WITH-NITS; the headers-incomplete spin guard was added per the review.
2026-06-30Implement the timer/garbage-collector (was disabled/unfinished)Paul Buetow
wrap.cpp had '//TIMR->run(); // TODO' but timr::run() was never defined (wouldn't compile if uncommented), and the intended timer thread (timr::start) ran in a pthread and would race with the single-threaded libevent main loop (no locking on the shared room/user maps). Net effect: idle timeouts never ran, ghost users lingered in the online list forever after disconnect, and message timestamps were always 00:00:00 (s_time was only updated by the never-run timer). Also, even single-threaded, check_timeout was unsafe: user::set_online(false) does p_room->del_elem during hashmap::run_func's live begin/end iteration -> iterator invalidation/crash. Fix: - hashmap::run_func (both overloads): snapshot the values into a vector before invoking the callback, so callbacks may safely delete from the map during iteration. - timr::tick(): single-threaded 1s-cadence tick (called from the libevent main loop) that updates s_time/s_uptime every tick, and at the top of each minute runs check_timeout (idle timeout + auto-away), posts a PING keepalive to all streams, every 10 min cleans the ip cache, hourly runs garbage collection. - main.cpp: register a periodic libevent timer (1s, EV_PERSIST) calling timr::tick() in the event loop - no new thread. B1 use-after-free (found by independent review, fixed): when an idle user is reaped it moves to garbage while its long-lived stream context is still alive; the hourly remove_garbage would delete the user, leaving the context's p_user dangling (UAF on later disconnect). Fixed by: - gcol::remove_garbage now keeps garbage users whose stream is still open (get_stream_fd() >= 0) until the stream closes, deleting only closed-stream users (collected via a new collect_users_ helper since the hash_map base is privately inherited and not directly iterable). - handle_stream_read now reaps the disconnecting user from its room (set_online(false), a no-op if already idle-reaped) so a clean disconnect immediately clears the online list instead of lingering until idle-reap. Also removed the debug 'cout << SETTING OFFLINE' spam in set_online. Verified: timestamps now real (e.g. 10:29:20 vs old 00:00:00); idle users reaped at the next top-of-minute tick (no crash); clean disconnect removes the user from the online list; idle-reaped-then-stream-closed does not UAF (0 restarts); normal multi-user chat still works. Two independent fresh-context reviews: APPROVE-WITH-NITS; the B1 UAF and disconnect-reap findings were addressed; the pre-existing flush_stream hard-error UAF edge was filed as a separate follow-up.
2026-06-30Fix predictable/colliding session IDs (same-second login crash + hijacking)Paul Buetow
sman::generate_id seeded rand() per call with time(0)+chat.session.kloakkey. Two problems: (1) two logins in the same wall-clock second produced identical tmpids; the collision retry then re-seeded with the same time and recursed forever -> stack overflow (SIGSEGV) -> DoS. (2) IDs were predictable (time-based seed + weak rand) -> session hijacking. Fix: generate IDs from /dev/urandom (one-time rand() fallback seeded with time^getpid, seeded once, not per call), and replace the unbounded recursion with a bounded retry loop (8 attempts). The give-up path returns a final candidate only if it does not collide (never overwriting/ leaking an existing session), else returns empty so login degrades gracefully instead of crashing. Also clear the urandom stream's failbit on a failed read so a transient failure self-heals, and guard i_len<=0. The runtime-disabled (md5hash=false) md5 transform block is left as-is; its separate bad-substr bug is out of scope here. Verified: 15 rapid same-second logins no longer crash (0 restarts); IDs are distinct; normal login + chat streaming still work. Independent fresh-context review: APPROVE-WITH-NITS; the give-up/leak, urandom-stuck, and i_len nits were addressed.
2026-06-30Fix unauth operator escalation -> /exec RCEPaul Buetow
In guest/no-DB mode (Mode A, no authentication) chat::login granted operator status (rang 0) to anyone who logged in with the nick matching chat.defaultop ('Snoop'). An operator can run /exec (mods/commands/ yc_exec.cpp) which does popen() on an attacker-controlled shell string, i.e. unauth -> RCE, and /set to re-enable disabled commands. Fix: gate the defaultop grant on p_user->get_is_reg(). In Mode A is_reg is only ever set under #ifdef DATABASE (compiled out), so no guest can become operator; the only set_status(0) path is now dead. Registered defaultops still get op once DB auth (Mode B) is enabled. Defense-in-depth (per independent review): also physically omit mods/commands/yc_exec.so from the revival image so the popen RCE primitive is absent, not merely unreachable. Verified in a container: logging in as 'Snoop' no longer grants op (/exec returns 'No such command', no command output); /time and normal guest use still work. Builds clean. Independent fresh-context review: APPROVE-WITH-NITS; the drop-yc_exec.so hardening was applied; blanking chat.defaultop was deliberately skipped to preserve the Mode B bootstrap-op path.
2026-06-30Fix path traversal (file read) and dlopen-traversal (RCE)Paul Buetow
Three path-traversal vulnerabilities, all caused by sanitizing the raw URL before url_decode (so %-encoded %2e%2e / %2f survive the check and decode to '..' afterwards), and by concatenating attacker-controlled names into filesystem paths: 1. Arbitrary file read via the HTML template path. src/sock/sock.cpp stripped the literal '/..' from the raw query before tool::url_decode, so 'GET /..%2f..%2fetc%2fpasswd' decoded to '../../etc/passwd' and src/html.cpp opened ifstream(templatedir + request), escaping html/. Confirmed: returned /etc/passwd, /etc/hosts, and the app's own etc/ychat.conf (leaking the MySQL password). Fix: add tool::path_has_traversal (rejects any '.'/'..' path component and embedded NULs on the DECODED path) and redirect to the notfound page in handle_client_read when it triggers. 2. Arbitrary shared-object load (RCE) via the html-module dlopen path. reqp::run_html_mod built 'htmldir + yc_ + s_event + .so' from the attacker-controlled event query param and dlopen()'d it; 'event=..%2f..' could load a .so outside the modules dir. Fix: reject non-alphanumeric s_event before building the path. 3. Same via the command-module dlopen path. user::command built 'commandsdir + yc_ + s_command2 + .so' from the first token of a '/' chat message and dlopen()'d it. Fix: reject non-alphanumeric s_command2 before building the path. Verified in a container: file-read traversal now returns the notfound page (not file contents); command/module-name traversal is blocked and not dlopen'd; legit /time, normal pages, login and streaming all still work. Builds clean. Independent fresh-context review: APPROVE-WITH-NITS; the dlopen-traversal and NUL-hardening findings were addressed here; the (disabled-by-default) CGI popen command-injection was noted as a separate follow-up.
2026-06-30conf: add text/css and image/x-icon content typesPaul Buetow
style.css was served with an empty Content-Type (httpd.contenttypes had no 'css' entry), so browsers refused to apply the stylesheet and the page rendered unstyled. Add css->text/css and ico->image/x-icon.
2026-06-30Reimplement the streaming chat-display layerPaul Buetow
The message-delivery layer was gutted (user::msg_post commented out; set_context/_send referenced only in comments, never defined), so the chat could log in but never show or send messages: the stream frame returned an empty response (502 behind Traefik) and posted messages went nowhere. Reimplemented server-streamed chat: - user: add i_stream_fd (long-lived chat-display connection) + b_stream_ready + s_msg buffer. msg_post appends to s_msg and flushes to the fd once the initial response is sent (best-effort non-blocking write; buffer on EAGAIN; on hard write error drop the reference and mark offline). flush_stream/clear_stream helpers; clear_stream on clean()/disconnect. - reqp (event=stream): build the initial HTTP response (headers, no Content-Length so it streams incrementally, + the parsed stream.html body), attach this fd to the user, set p_context->p_user, KEEP_ALIVE=yes. - sock: route KEEP_ALIVE responses through handle_stream_write (sends the initial page, then arms a persistent read event) and handle_stream_read (drains stray bytes; on EOF/error reaps the context and clears the user's stream). The connection stays open for pushed messages. - context: init p_user=NULL; track i_buf_len across reads (POST body behind a proxy arrives in a separate segment). Verified locally: login -> open stream -> post message -> message appears in the stream; two users in a room both receive a posted message.
2026-06-30sock: read full request across segments (fix POST body behind proxy)Paul Buetow
handle_client_read did a single read() and assumed the whole request (headers + POST body) arrived in one packet. Behind a reverse proxy (Traefik) the body is forwarded in a separate TCP segment, so the form params (event/nick/tmpid/...) were never parsed: logins silently no-op'd, the frameset was served with tmpid= (empty), and every frame hit 'Session: Could not find session' -> 502 in all frames. Now accumulate into c_buf across re-arms of the read event (track i_buf_len in the context), drain non-blocking until EAGAIN, and for POST wait until the headers AND the full Content-Length body are present before parsing. EOF/empty and hard read errors are handled without crashing.
2026-06-30Stop tracking runtime log/ artifactsPaul Buetow
2026-06-30gitignore build/runtime artifacts (obj, bin, log, mods)Paul Buetow
2026-06-30Fix runtime crashes: logd recursion, trim OOB, sock SO_REUSEADDR/accept, md5 ↵Paul Buetow
session Multiple latent bugs made the chat crash on real use (login POST segfaulted): - logd::flush: when a log file can't be opened, log the error to stderr and exit(1). Previously it called wrap::system_message, which routes back through LOGD->log_simple_line->flush on the same failing logd -> infinite recursion -> stack overflow (SIGSEGV). Bitten in k8s where an emptyDir on /app/log hides the image's /app/log/rooms, so the room log open failed on login. - tool::trim: rewrite the right-trim; the original did s_str[s_str.size()] (out-of-bounds under _GLIBCXX_ASSERTIONS / UB) and erased at i_pos=size. - sock::_make_server_socket: move SO_REUSEADDR setsockopt BEFORE bind so a quick container restart rebinds port 2000 (was EADDRINUSE -> fallback to 2001, which the k8s Service doesn't target -> 502). - sock::process_request: init accept() addrlen and bail on any accept error (not just EAGAIN/EINTR) so we never proceed with fd=-1 (EBADF). - chat.session.md5hash=false at runtime: the md5 session-id path does s_hash.substr(s_ret.find(s_salt)+salt.len()+3); the default salt has chars not in chat.session.validchars so find() returns npos and the substr/append corrupts the heap and segfaults on login. Overridden via -o in the image CMD. - docker-entrypoint.sh: mkdir -p /app/log/rooms at startup (volume mount hides the image's copy).
2026-06-30reqp: write the built HTTP/1.1 header+body back to p_responsePaul Buetow
parse() constructed the full 'HTTP/1.1 200 OK ... Content-Length ... Content-Type' response into a local s_resp but never assigned it back, so the socket writer sent only the body (HTTP/0.9-style). Traefik/proxies 500'd on these responses. This makes ychat emit proper HTTP/1.1 responses so the f3s ingress can proxy it.
2026-06-30Docker revival (Mode A): build in container, deploy to f3sPaul Buetow
Multi-stage Dockerfile (Rocky 9 builder + runtime) builds ychat entirely in a container with SSL/MySQL/readline disabled (in-memory guest chat). Three minimal legacy-C++ patches so it builds on GCC 11: - glob.h/modl.cpp: rename project 'function' typedef to 'mod_func_t' (collides with std::function via 'using namespace std') - logd.cpp: 'ofstream == NULL' -> '!is_open()' - sock.cpp: assign i_server_sock before set_nonblock() (was EBADF on startup) Add DOCKER.md with build/push/deploy workflow and quirks (HTTP/0.9 responses, ephemeral in-memory state).
2010-11-21renamed branche into kind in order not to confuse with subversion branchesPaul Buetow
2010-11-21added yhttpd and ycurses trunk versionsPaul Buetow
2010-11-21(no commit message)Paul Buetow
2010-11-21moving into ychat subdirPaul Buetow
2010-11-21(no commit message)Paul Buetow
2010-11-21moving stuff to branchesPaul Buetow
2010-11-21moving stuff to branchesPaul Buetow
2009-01-27(no commit message)Paul Buetow
2009-01-27(no commit message)Paul Buetow
2009-01-27(no commit message)Paul Buetow
2009-01-27(no commit message)Paul Buetow
2008-05-15trunkPaul Buetow
2008-05-15moved stuff the trunkPaul Buetow