HTTP and WebSocket server run from Chrome browser using Direct Sockets TCPServerSocket
WTFPL License
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.
node:crypto
implementation of Ed25519 algorithmnode
, deno
, bun
AbortController
when the WebSocket client closes the connection. Completed.ArrayBuffer
, DataView
, TypedArray
for Node.js Buffer polyfillbun install
or
npm install
or
deno add npm:wbn
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
.
deno -A generateWebCryptoKeys.js
Write signed.swbn
to current directory
Node.js
node --experimental-default-type=module index.js
Bun
bun run index.js
Deno
deno -A --unstable-byonm index.js
node_module
folder and create the .swbn
file and IWAdeno -A --import-map=import-map.json --unstable-byonm index.js
wbn-bundle.js
from webbundle-plugins/packages/rollup-plugin-webbundle/src/index.ts
with bun
git clone https://github.com/GoogleChromeLabs/webbundle-plugins
cd webbundle-plugins/packages/rollup-plugin-webbundle
bun install -p
src/index.ts
comment line 18, : EnforcedPlugin
, line 32 const opts = await getValidatedOptionsWithDefaults(rawOpts);
and lines 65-121, because I will not be using Rollupbun build --target=node --format=esm --sourcemap=none --outfile=webpackage-bundle.js ./webbundle-plugins/packages/rollup-plugin-webbundle/src/index.ts
node:crypto
directly import { webcrypto } from "node:crypto";
/node_modules/wbn-sign/lib/utils/utils.js
use switch (key.algorithm.name) {
getRawPublicKey
becomes an async
function for substituting const exportedKey = await webcrypto.subtle.exportKey("spki", publicKey);
for publicKey.export({ type: "spki", format: "der" });
/node_modules/wbn-sign/lib/signers/integrity-block-signer.js
use const publicKey = await signingStrategy.getPublicKey();
and [getPublicKeyAttributeName(publicKey)]: await getRawPublicKey(publicKey)
; verifySignature()
also becomes an async
function where const algorithm = { name: "Ed25519" }; const isVerified = await webcrypto.subtle.verify(algorithm, publicKey, signature, data);
is substituted for const isVerified = crypto2.verify(undefined, data, publicKey, signature);
/node_modules/wbn-sign/lib/web-bundle-id.js
serialize()
function becomes async
for return base32Encode(new Uint8Array([...await getRawPublicKey(this.key), ...this.typeSuffix]), "RFC4648", { padding: false }).toLowerCase();
; and serializeWithIsolatedWebAppOrigin()
becomes an async
function for return ${this.scheme}${await this.serialize()}/;
; toString()
becomes an async
function for return Web Bundle ID: ${await this.serialize()} Isolated Web App Origin: ${await this.serializeWithIsolatedWebAppOrigin()};
src/index.ts
export {WebBundleId, bundleIsolatedWebApp};
index.js
, the entry point for how I am creating the SWBN and IWA I get the public and private keys created with Web Cryptography API, and use Web Cryptography API to sign and verifyNavigate 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: 44818,
});
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:44818", {
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:44818");
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