Skip to content

Instantly share code, notes, and snippets.

@guest271314
Last active October 21, 2024 05:29
Show Gist options
  • Save guest271314/73a50e9ebc6acaaff5d39f6fc7918ebf to your computer and use it in GitHub Desktop.
Save guest271314/73a50e9ebc6acaaff5d39f6fc7918ebf to your computer and use it in GitHub Desktop.
How to use the browser as an HTTP over TCP socket server

Today we will be using the Web browser (Google Chrome for Testing 125) as a local HTTP over TCP socket server.

WICG Direct Sockets specifies an API that provides TCPSocket, UDPSocket, and TCPServerSocket.

In Chromium based browsers, for example Chrome, this capability is exposed in an Isolated Web Apps (IWA).

Previously we have created an IWA that we launch from arbitrary Web sites with open(), including SDP from a RTCDataChannel in query string of the URL, created in the arbotrary Web page, and exchanged signals with the RTCDataChannel created in the IWA window using WICG File System Access for the ability to send data to the IWA which is then passed to a TCPSocket instance for that sends the data to a Node.js, Deno, Bun, or txiki.js TCP socket server for processing, then sends the processed data back to the Web page using RTCDataChannel in each window, see telnet-client (user-defined-tcpsocket-controller-web-api branch), which is a fork of telnet-client.

Now we will use the browser itself as a local HTTP server over the TCPServerSocket interface.

Let's start with HTTP

Basic aspects of HTTP

HTTP is simple

HTTP is generally designed to be simple and human-readable, even with the added complexity introduced in HTTP/2 by encapsulating HTTP messages into frames. HTTP messages can be read and understood by humans, providing easier testing for developers, and reduced complexity for newcomers.

We'll also note this claim on the MDN Web Docs page from Client: the user-agent

The browser is always the entity initiating the request. It is never the server (though some mechanisms have been added over the years to simulate server-initiated messages).

which is no longer the case, as we'll demonstrate below, in code.

Some further reading about HTTP can be found here HTTP - Hypertext Transfer Protocol.

The reason for and use of the Access-Control-Request-Private-Network and Access-Control-Allow-Private-Network headers can be found here Private Network Access: introducing preflights.

An excellent article and example of a basic HTTP server with comments explaining what is going on, including comments in the code, written in C, can be found here Making a simple HTTP webserver in C. We have previously used that example to create a simple HTTP Web server for QuickJS, which does not include a built-in Web server in the compiled qjs executable, see webserver-c (quickjs-webserver branch).

We are just going to read the full request, headers and body, use a RegExp to match the body of a POST request, convert the text we post to uppercase letters, then respond with the uppercase letters.

We'll leave it to the reader to implement processing FormData (multipart/form-data), and other features, including novel ways to stream data in the server.

Currently no browser supports full-duplex streaming using fetch(), see See Fetch body streams are not full duplex #1254 - except for the case of between a ServiceWorker and a WindowClient on Chromium-based browsers, i.e., Chrome; here's an example of that exception Half duplex stream. We have previously used Native Messaging to implement full duplex streaming using Deno's and Node.js' fetch() implementation from the browser, see native-messaging-deno (fetch-duplex branch) and native-messaging-nodejs (full-duplex branch). Bun does not currently implement HTTP/2 for client or server, see Implement HTTP2 server support to enable gRPC #8823.

We'll use Transfer-Encoding: chunked in our browser-based server, just in case we want to serve a chunked response, e.g., media to be appended to a MediaSource instance during the read of the stream from fetch(). This we can do without an HTTP/2 or QUIC server to stream data from the server to the client.

Our request from DevTools console of an arbitrary Web page looks something like this

fetch("http://0.0.0.0:8000", {
    method: "post",
    body: "test",
    headers: {
      "Access-Control-Request-Private-Network": true
    }
  })
  .then((r) => r.text()).then((text) => console.log({
    text
  })).catch(console.error);

Our Direct Sockets TCPServerSocket that is out HTTP server in the browser looks something like this

const decoder = new TextDecoder();

const encoder = new TextEncoder();

const encode = (text) => encoder.encode(text);

const socket = new TCPServerSocket("0.0.0.0", {
  localPort: 8000,
});

console.log({
  socket
});

const {
  readable: server,
  localAddress,
  localPort,
} = await socket.opened;

await server.pipeTo(
  new WritableStream({
    async write(connection) {
      console.log({
        connection
      });
      const {
        readable: client,
        writable,
        remoteAddress,
        remotePort,
      } = await connection.opened;
      const writer = writable.getWriter();
      console.log({
        remoteAddress,
        remotePort,
      });
      await client.pipeThrough(new TextDecoderStream()).pipeTo(
        new WritableStream({
          async write(request, controller) {
            console.log({
              request
            });
            if (/^OPTIONS/.test(request)) {
              await writer.write(encode("HTTP/1.1 204 OK\r\n"));
              await writer.write(
                encode(
                  "Access-Control-Allow-Headers: Access-Control-Request-Private-Network\r\n",
                ),
              );
              await writer.write(
                encode("Access-Control-Allow-Origin: *\r\n"),
              );
              await writer.write(
                encode("Access-Control-Allow-Private-Network: true\r\n"),
              );
              await writer.write(
                encode(
                  "Access-Control-Allow-Headers: Access-Control-Request-Private-Network\r\n\r\n",
                ),
              );
              await writer.close();
            }
            if (/^(POST|query)/i.test(request)) {
              const [body] = request.match(
                /(?<=\r\n\r\n)[a-zA-Z\d\s\r\n-:;=]+/igm,
              );
              console.log({
                body,
              });
              await writer.write(encode("HTTP/1.1 200 OK\r\n"));
              await writer.write(
                encode("Content-Type: application/octet-stream\r\n"),
              );
              await writer.write(
                encode("Access-Control-Allow-Origin: *\r\n"),
              );
              await writer.write(
                encode("Access-Control-Allow-Private-Network: true\r\n"),
              );
              await writer.write(
                encode(
                  "Access-Control-Allow-Headers: Access-Control-Request-Private-Network\r\n",
                ),
              );
              await writer.write(encode("Cache-Control: no-cache\r\n"));
              await writer.write(encode("Connection: close\r\n"));
              await writer.write(
                encode("Transfer-Encoding: chunked\r\n\r\n"),
              );
              const chunk = encode(body.toUpperCase());
              const size = chunk.buffer.byteLength.toString(16);
              await writer.write(encode(`${size}\r\n`));
              await writer.write(chunk.buffer);
              await writer.write(encode("\r\n"));
              await writer.write(encode("0\r\n"));
              await writer.write(encode("\r\n"));
              await writer.close();
              await writer.closed;
            }
          },
          close() {
            console.log("Client closed");
          },
          abort(reason) {
            console.log("Client aborted");
          },
        }),
      ).catch(console.error);
    },
    close() {
      console.log("Host closed");
    },
    abort(reason) {
      console.log("Host aborted", reason);
    },
  }),
).catch(console.error);

We've just implemented a local HTTP over TCP socket server in and from the browser.

Happy hacking. Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment