]> mop.ddnsfree.com - git repositories - brisk.git/commitdiff
alternative mode: nginx speaks http directly with the daemon
authorMatteo Nastasi <nastasi@alternativeoutput.it>
Sun, 13 Sep 2026 10:33:07 +0000 (12:33 +0200)
committerMatteo Nastasi <nastasi@alternativeoutput.it>
Sun, 13 Sep 2026 10:33:07 +0000 (12:33 +0200)
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE

web/Obj/sac-a-push.phh
web/spush/brisk-spush.phh

index 25f5bfcd81395d8024aa0888e72fd5af219cc754..1a9a413aed1eea9ef159836dc9998ff500dca97b 100644 (file)
@@ -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:<request line>\n <header>\n ... \n\n <body, if any>
+
+   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) {
index c4dca71f1f054265cd751340767d917cb67e644c..39bcee3bdab11c63abc3e433c0c594bb71928c0e 100644 (file)
@@ -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)
   {