From: Matteo Nastasi Date: Sun, 13 Sep 2026 10:33:07 +0000 (+0200) Subject: alternative mode: nginx speaks http directly with the daemon X-Git-Url: https://mop.ddnsfree.com/gitweb/?a=commitdiff_plain;h=3394dc18af1d4759333e15e0c0744821b585f109;p=brisk.git alternative mode: nginx speaks http directly with the daemon Prototype of an architecture that removes the descriptor handover. Turned on with SPU_HTTP_DIRECT in brisk-spush.phh; the default stays FALSE, that is the historic behaviour, and the two modes live side by side. Today the descriptor the daemon receives is the one of the nginx->apache connection, already in clear because nginx stripped the TLS one hop earlier. With this mode nginx opens an ordinary http connection on the unix socket and the daemon uses it directly. What it takes away: - php-ancillary, the C extension - mod_proxy_fdpass2, the apache module - ngx_http_fdpass_module, the nginx module - apache as an intermediate layer - the kTLS requirement, which would be needed if nginx were to hand over the browser descriptor (encrypted) instead of the cleartext one towards apache That is three pieces of bespoke C, each of which needed a port in this very migration, plus a kernel requirement. The daemon becomes an ordinary http server behind a reverse proxy. The code: - spu_head_end() and spu_head_to_info() repackage the request read from the network in the same format the control channel produced ("The-Request:" in front of the request line), so that spu_process_info() does not know where the data comes from - the head of the request is NOT read by blocking: there is a single event loop for every player. The connection is registered in the PENDINGPAGE_WAITHEAD state among the watched sockets and completed incrementally, reusing the machinery already in place for the bodies of partial POSTs; if the body is still missing, it moves on to pendpage_try_addwait seamlessly - trim() on the header value: it was missing, and real http writes "Name: value" with a space, which would have ended up inside the value, breaking the cookies and the comparisons on Upgrade Checked in the container with nginx 1.26.3 and php 8.4, without apache: the page, an authenticated login, comet streaming, POST, and a complete game over HTTPS with five players, the auction, 40 cards and the score saved on postgresql. Secure websockets work (101 Switching Protocols and frames from the daemon), confirmed from chrome too. Zero errors in the daemon. Not verified yet: behaviour under load, and the reuse of connections between nginx and the daemon (in the tests keepalive towards the upstream is off). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE --- diff --git a/web/Obj/sac-a-push.phh b/web/Obj/sac-a-push.phh index 25f5bfc..1a9a413 100644 --- a/web/Obj/sac-a-push.phh +++ b/web/Obj/sac-a-push.phh @@ -132,6 +132,46 @@ function post_manage(&$post, $line) } } +/* + * "Direct http" mode: nginx (or any reverse proxy) opens an ordinary http + * connection on the unix socket instead of handing over the client file + * descriptor. The head of the request therefore comes from the network, and + * must be read without ever blocking the event loop: the loop itself takes + * care of it, with a pending page in the PENDINGPAGE_WAITHEAD state. These + * two functions are its two steps. + */ + +/* Offset right after the empty line that closes the head, or FALSE if it + has not arrived in full yet. */ +function spu_head_end($buf) +{ + if (($pos = strpos($buf, "\r\n\r\n")) !== FALSE) { + return ($pos + 4); + } + if (($pos = strpos($buf, "\n\n")) !== FALSE) { + return ($pos + 2); + } + return (FALSE); +} + +/* Repackages the http request in the same format the control channel of + mod_proxy_fdpass2 used to produce: + + The-Request:\n
\n ... \n\n + + so that spu_process_info() need not know where the data came from. The + request line has no colon, and without the label the parser would not + recognise it. */ +function spu_head_to_info($buf, $sep) +{ + $body = mb_substr($buf, $sep, NULL, "ASCII"); + $headpart = mb_substr($buf, 0, $sep, "ASCII"); + $lines = preg_split("/\r?\n/", rtrim($headpart, "\r\n")); + $lines[0] = "The-Request:" . $lines[0]; + + return (implode("\n", $lines) . "\n\n" . $body); +} + function spu_process_info($stream_info, &$method, &$header, &$get, &$post, &$cookie, &$rest, &$cont) { $check_post = FALSE; @@ -213,8 +253,15 @@ function spu_process_info($stream_info, &$method, &$header, &$get, &$post, &$coo continue; } $split = explode(":", $line, 2); + if (!isset($split[1])) { + continue; + } $hea_id = trim(mb_convert_case($split[0], MB_CASE_TITLE, 'UTF-8')); - $header[$hea_id] = $split[1]; + /* the value needs trimming: in the control channel dump it arrives + as "Name:value", but in a real http request it is "Name: value" + and the space would end up inside the value, breaking the cookie + parsing and the comparisons on Upgrade. */ + $header[$hea_id] = trim($split[1]); } return $path; } @@ -849,7 +896,27 @@ class Sac_a_push { $cookie = array(); $rest = 0; $cont = ""; - if (($new_socket = ancillary_getstream($new_unix, $stream_info)) !== FALSE) { + /* Two ways of getting the client connection: + - direct http: nginx opens an ordinary http connection on the unix + socket, and the accepted one is already the one to use + - descriptor handover: the frontend passes the client socket with + SCM_RIGHTS plus a channel carrying the headers (needs the + ancillary extension and, without a cleartext layer in front, + kTLS) */ + if (defined('SPU_HTTP_DIRECT') && SPU_HTTP_DIRECT) { + /* The head of the request has not arrived yet and cannot be + waited for here: there is a single event loop for every + player, and a blocking read would stop them all. The + connection is added to the watched ones in the WAITHEAD + state; the loop will complete it when select() reports it + readable. */ + stream_set_blocking($new_unix, 0); + $this->pendpage_add( + PendingPage::pendingpage_waithead($new_unix, $this->curtime, 20)); + continue; + } + $new_socket = ancillary_getstream($new_unix, $stream_info); + if ($new_socket !== FALSE) { // printf("NEW_SOCKET: %d\n", intval($new_socket)); stream_set_blocking($new_socket, $this->blocking_mode); // Set the stream to non-blocking // error_log(sprintf("RECEIVED HEADER:\n%s", $stream_info)); @@ -1060,7 +1127,49 @@ class Sac_a_push { fprintf(STDERR, "User associated with ID: %s not found\n", $id); } - if (isset($this->s2p[$id])) { + /* direct http mode: the head of the request arrives in + pieces like any other data, and here it is + accumulated without ever blocking */ + if (isset($this->s2p[$id]) + && $this->s2p[$id]->status == PENDINGPAGE_WAITHEAD) { + $pp = $this->s2p[$id]; + $pp->cont .= $buf; + $sep = spu_head_end($pp->cont); + if ($sep === FALSE) { + if (mb_strlen($pp->cont, "ASCII") > 32768) { + $this->pendpage_rem($pp); + fclose($sock); + $this->socks_unset($sock); + } + continue; /* testa incompleta: si riprende al prossimo giro */ + } + $stream_info = spu_head_to_info($pp->cont, $sep); + $new_socket = $pp->socket_get(); + $this->pendpage_rem($pp); + + $method = ""; $get = array(); $post = array(); + $cookie = array(); $rest = 0; $cont = ""; + if (($path = spu_process_info($stream_info, $method, $header, + $get, $post, $cookie, $rest, $cont)) == FALSE) { + fclose($new_socket); + $this->socks_unset($new_socket); + continue; + } + $addr = (array_key_exists('X-Real-Ip', $header) ? $header['X-Real-Ip'] + : addrtoipv4(stream_socket_get_name($new_socket, TRUE))); + $addr = $this->pproxy_realip($header, $addr); + + if ($method == "POST" && $rest > 0) { + /* the body is still missing: the machinery already in + place for partial POSTs takes over */ + $this->pendpage_try_addwait($new_socket, 20, $method, $header, + $get, $post, $cookie, $path, $addr, + $rest, $cont); + continue; + } + $manage_page = TRUE; + } + else if (isset($this->s2p[$id])) { $this->s2p[$id]->rest -= mb_strlen($buf, "ASCII"); $this->s2p[$id]->cont .= $buf; if ($this->s2p[$id]->rest <= 0) { diff --git a/web/spush/brisk-spush.phh b/web/spush/brisk-spush.phh index c4dca71..39bcee3 100644 --- a/web/spush/brisk-spush.phh +++ b/web/spush/brisk-spush.phh @@ -26,11 +26,20 @@ $DOCUMENT_ROOT=""; $HTTP_HOST="dodo.birds.lan"; define('USOCK_PATH_PFX', "/tmp/brisk"); define('USOCK_POOL_N', 10); + +/* TRUE = nginx speaks http directly on the unix socket (no ancillary + extension, no descriptor handover modules, no kTLS) + FALSE = historic behaviour: the frontend hands over the client descriptor */ +define('SPU_HTTP_DIRECT', FALSE); define('SOCK_SHARD_N', 2); define('PENDINGPAGE_CONTINUE', 0); define('PENDINGPAGE_WAITDATA', 1); define('PENDINGPAGE_FLUSH', 2); +/* waiting for the head of the http request: used only in SPU_HTTP_DIRECT + mode, where the daemon has to read it from the network and cannot do so by + blocking the event loop, which serves every player */ +define('PENDINGPAGE_WAITHEAD', 3); class PendingPage { public $socket; // socket handler of page stream @@ -94,6 +103,17 @@ class PendingPage { $this->msg_sz = $hea_sz; } + /* page waiting for the head: nothing is known about the request yet, + bytes are accumulated in ->cont until the empty line arrives */ + static function pendingpage_waithead($socket, $curtime, $kalive) + { + $thiz = new PendingPage($socket, $curtime, $kalive); + $thiz->status = PENDINGPAGE_WAITHEAD; + $thiz->cont = ""; + $thiz->rest = 0; + return ($thiz); + } + static function pendingpage_waiting($socket, $curtime, $kalive, $method, $header, $get, $post, $cookie, $path, $addr, $rest, $cont) {