HTTP and WebSocket server run from Chromium and Chrome browsers using Direct Sockets TCPServerSocket
.
WICG Direct Sockets specifies an API
that provides TCPSocket
, UDPSocket
, and TCPServerSocket
. Prior art: chrome.socket.
In Chromium based browsers, for example Chrome, this capability is exposed in 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 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 HTTP and WebSocket server over the TCPServerSocket
interface.
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).
is not technically accurate, 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 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).
For the WebSocket implementation WebSocket - binary broadcast example (pure NodeJs implementation without any dependency) is used.
- Substitute Web Cryptography API (wbn-sign-webcrypto) for
node:crypto
implementation of Ed25519 algorithm - Install and run same JavaScript source code in different JavaScript runtimes, e.g.,
node
,deno
,bun
- Create valid close frame (server to client) for WebSocket server; currently we abort the request in the server with
AbortController
when the WebSocket client closes the connection. Completed. - Substitute
ArrayBuffer
,DataView
,TypedArray
for Node.js Buffer polyfill - TLS and HTTP/2 support
- Create Signed Web Bundle and Isolated Web App in the browser
Creates a node_modules
folder containing dependencies
bun install
or
npm install
or
deno run -A deno_install.js
Entry point is assets
directory which contains index.html
, script.js
, .well-known
directory with manifest.webmanifest
, and any other scripts or resources to be bundled.
This only has to be done once. generateWebCryptoKeys.js
can be run with node
, deno
, or bun
.
node --experimental-default-type=module generateWebCryptoKeys.js
Write signed.swbn
to current directory
Node.js
node --experimental-default-type=module index.js
Bun
bun run index.js
Deno
deno run --unstable-byonm -A index.js
Dynamically fetch dependencies without creating a node_modules
folder and create the .swbn
file and IWA.
deno run -A --unstable-byonm --import-map=import-map.json index.js
try {
console.log(
await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: ".",
sourcemap: "external",
splitting: false,
target: "bun" // or "node"
format: "esm",
// minify: true,
external: ["mime", "base32-encode", "wbn-sign-webcrypto", "wbn"],
naming: {
entry: "[dir]/wbn-bundle.[ext]",
},
}),
);
} catch (e) {
console.log(e);
}
Navigate to chrome://web-app-internals/
, on the line beginning with Install IWA from Signed Web Bundle:
click Select file...
and select signed.swbn
.
See https.js
and ws.js
in examples
directory.
We could recently open the IWA window
from arbitrary Web sites in DevTools console
or Snippets with
var iwa = open("isolated-app://<IWA_ID>");
iwa: Mark isolated-app: as being handled by Chrome evidently had the side effect of blocking that capability, see window.open("isolated-app://") is blocked. isolated-web-app-utilities provides approaches to open the IWA window from arbitrary Web sites, chrome:
, chrome-extension:
URL's.
const socket = new TCPServerSocket("0.0.0.0", {
localPort: 8080,
});
const {
readable: server,
localAddress,
localPort,
} = await socket.opened;
console.log({ server });
// TODO: Handle multiple connections
await server.pipeTo(
new WritableStream({
async write(connection) {
const {
readable: client,
writable,
remoteAddress,
remotePort,
} = await connection.opened;
console.log({ connection });
const writer = writable.getWriter();
console.log({
remoteAddress,
remotePort,
});
const abortable = new AbortController();
const { signal } = abortable;
// Text streaming
// .pipeThrough(new TextDecoderStream())
await client.pipeTo(
new WritableStream({
start(controller) {
console.log(controller);
},
async write(r, controller) {
// Do stuff with encoded request
const request = decoder.decode(r);
console.log(request);
// HTTP and WebSocket request and response logic
// Create and send valid WebSocket close frame to client
await writer.write(new Uint8Array([0x88, 0x00])); // 136, 0
await writer.close();
return await writer.closed;
},
close: () => {
console.log("Client closed");
},
abort(reason) {
console.log(reason);
},
})
, {signal}).catch(console.warn);
},
close() {
console.log("Host closed");
},
abort(reason) {
console.log("Host aborted", reason);
},
}),
).then(() => console.log("Server closed")).catch(console.warn);
};
Using WHATWG Fetch
fetch("http://0.0.0.0:8080", {
method: "post",
body: "test",
headers: {
"Access-Control-Request-Private-Network": true,
},
})
.then((r) => r.text()).then((text) =>
console.log({
text,
})
).catch(console.error);
var wss = new WebSocketStream("ws://0.0.0.0:8080");
console.log(wss);
wss.closed.catch((e) => {});
wss.opened.catch((e) => {});
var {
readable,
writable,
} = await wss.opened.catch(console.error);
var writer = writable.getWriter();
var abortable = new AbortController();
var {
signal,
} = abortable;
// .pipeThrough(new TextDecoderStream())
var pipe = readable.pipeTo(
new WritableStream({
start(c) {
console.log("Start", c);
},
async write(v) {
console.log(v, decoder.decode(v));
},
close() {
console.log("Socket closed");
},
abort(reason) {
// console.log({ reason });
},
}),
{
signal,
},
).then(() => ({ done: true, e: null })).catch((e) => ({ done: true, e }));
var encoder = new TextEncoder();
var decoder = new TextDecoder();
var encode = (text) => encoder.encode(text);
await writer.write(encode("X"));
// Later on close the WebSocketStream connection
await writer.close().catch(() => pipe).then(console.log);
Do What the Fuck You Want to Public License WTFPLv2