• websrvr: HTTPS request split across TLS records is dropped (session cl

    From Rob Swindell@1:103/705 to GitLab issue in main/sbbs on Wed Sep 30 19:59:46 2026
    open https://gitlab.synchro.net/main/sbbs/-/work_items/1275

    ## Summary

    An HTTPS request whose request line (or headers) arrives in more than one TLS record is silently dropped: the session is closed with no response, no error, and no `Request N:` log line. The same request over plain HTTP is served normally.

    Observed on Linux, v3.22a Debug, master as of 2026-09-30.

    ## Reproduction

    Scratch web server on loopback (`[Web] Port=18080 TLSPort=18443`, static `index.html`). Python client; the request is `GET /index.html HTTP/1.0\r\nHost: localhost\r\n\r\n`, sent either whole or as the first 5 bytes, a 1 s pause, then the rest:

    ```
    http single -> HTTP/1.1 200 OK
    http split -> HTTP/1.1 200 OK
    https single -> HTTP/1.1 200 OK
    https split -> (no response; server closed the connection)
    ```

    Server log for the failing case has only `Session thread started`, then `Session thread terminated after 0 requests`. No `Request 1:` line, no TLS error at `LogLevel=Debug`.

    Any HTTPS client that does not deliver the whole request line in one TLS record hits this: a request typed into `openssl s_client`, or a client that flushes the request line before the headers.

    ## Cause

    `sockreadline()` in `websrvr.cpp` reads the request line one byte at a time via `sess_recv()`. For a TLS session it waits on the raw socket with `socket_readable()` only while `tls_pending` is false; once the first record is readable it sets `tls_pending` and from then on calls `cryptPopData()` directly, which is non-blocking (`CRYPT_OPTION_NET_READTIMEOUT` is 0 for the session).

    When the buffered record is drained before a newline has been seen, the next pop returns no data. `sess_recv()` turns that into `-1` (`len == 0` -> `tls_pending = false; len = -1`), and `sockreadline()` treats `-1` on a TLS session as a disconnect: it calls `close_session_socket()` and returns `-1`. The partial line is discarded and the session ends.

    The `tls_pending` short-circuit was added so a request body already decrypted in the TLS layer is not waited for on the raw socket (#1169). The missing piece is the other direction: when the TLS layer has nothing buffered and the socket is still open, `sockreadline()` should go back to waiting on the socket (clear `tls_pending` and loop) rather than closing. The same pattern is in `recvbufsocket()`/`get_req()` for the body read and should be checked too.

    ## Found while

    Investigating the web server stop hang of 2026-09-30 (the shutdown() kick commit on master). A test client sending a partial request line over TLS never reached the request-read wait because the session was closed on the second record.

    - *Authored by Claude (Claude Code), on behalf of @rswindell*
    --- SBBSecho 3.38-Linux
    * Origin: Vertrauen - [vert/cvs/bbs].synchro.net (1:103/705)