3
\$\begingroup\$

I'm using a Web extension to communicate with an Isolated Web App over externally_connectable for the capability to use Direct Sockets UDP and TCP sockets from arbitrary Web pages in DevTools.

Here's the code

In the Isolated Web App


document.title = "DirectSocket";
const USER_AGENT = "Built with Bun/1.2.22";
const EXTENSION_ID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
console.log(USER_AGENT, EXTENSION_ID);
globalThis.encoder = new TextEncoder();
globalThis.nativeSocket = null;
globalThis.nativeSocketReadable = null;
globalThis.nativeSocketWritable = null;
globalThis.nativeSocketWriter = null;
globalThis.nativeSocketAbortable = null;
globalThis.socketType = "";
const port = chrome.runtime.connect(EXTENSION_ID, {
 name: "iwa",
});
port.onMessage.addListener(async (message) => {
 globalThis.socketType = message.socketType;
 if (!globalThis.nativeSocket) {
 if (message.socketType === "tcp") {
 globalThis.nativeSocket = new TCPSocket(
 message.remoteAddress,
 message.remotePort,
 { noDelay: true, keepAliveDelay: 60 * 60 * 24 * 1000 },
 );
 }
 if (message.socketType === "udp") {
 globalThis.nativeSocket = new UDPSocket({
 remoteAddress: message.remoteAddress,
 remotePort: message.remotePort,
 });
 }
 console.log(globalThis.nativeSocket);
 nativeSocket.closed.then(() => console.log("socket closed")).catch((e) => console.log("opened", e));;
 const p = await nativeSocket.opened.catch((e) => console.log("opened", e));
 globalThis.nativeSocketReadable = p.readable;
 globalThis.nativeSocketWritable = p.writable;
 Promise.allSettled([globalThis.nativeSocketReadable.closed, globalThis.nativeSocketWritable.closed])
 .then((result) => console.log({result}));
 globalThis.nativeSocketAbortable = new AbortController();
 globalThis.nativeSocketWriter = globalThis.nativeSocketWritable.getWriter();
 globalThis.nativeSocketAbortable.signal.onabort = (e) => {
 globalThis.nativeSocket = null;
 globalThis.nativeSocketReadable = null;
 globalThis.nativeSocketWritable = null;
 globalThis.nativeSocketWriter = null;
 globalThis.nativeSocketAbortable = null;
 };
 globalThis.nativeSocketWriter.closed.then(() => {
 console.log("writer closed");
 if (globalThis.nativeSocketAbortable !== null) {
 globalThis.nativeSocketAbortable.abort("reason");
 }
 }).catch((e) => {
 console.log("writer closed", e);
 globalThis.nativeSocketAbortable.abort(e);
 });
 console.log(globalThis.nativeSocket);
 globalThis.nativeSocketReadable.pipeTo(
 new WritableStream({
 start() {
 const {
 localAddress,
 localPort,
 remoteAddress,
 remotePort,
 } = p;
 port.postMessage({
 localAddress,
 localPort,
 remoteAddress,
 remotePort,
 });
 document.body.insertAdjacentHTML(
 "afterbegin",
 `<pre>${
 JSON.stringify(
 {
 localAddress,
 localPort,
 remoteAddress,
 remotePort,
 },
 null,
 2,
 )
 }
 </pre>`,
 );
 },
 write(data) {
 port.postMessage(data?.data ? data : [...data]);
 },
 close() {
 console.log("close");
 },
 abort(reason) {
 console.log({ reason });
 },
 }),
 { signal: globalThis.nativeSocketAbortable.signal },
 )
 .then(() => console.log("stream closed"))
 .catch((e) => console.log("stream closed for " + e));
 } else {
 if (
 globalThis.nativeSocket instanceof UDPSocket &&
 Object.hasOwn(message, "data")
 ) {
 await globalThis.nativeSocketWriter.write(
 { data: new Uint8Array(Object.values(message.data)) },
 ).catch((e) => console.log("writer", e));
 }
 if (globalThis.nativeSocket instanceof TCPSocket) {
 await globalThis.nativeSocketWriter.write(
 new Uint8Array(message),
 ).catch((e) => console.log("writer", e));
 }
 }
});
port.onDisconnect.addListener((p) => {
 if (chrome.runtime?.lastError) {
 console.log(chrome.runtime.lastError);
 }
 console.log(p.name + " disconnected");
 try {
 globalThis.nativeSocketWriter.close()
 .catch((e) => console.log("writer close()", e));
 globalThis.nativeSocketAbortable.abort("reason");
 } catch (e) {
 console.log(e, "caught");
 console.trace(e);
 }
});

In the MV3 Web extension

addEventListener("install", async (e) => {
 console.log(e.type);
 e.addRoutes({
 condition: {
 urlPattern: new URLPattern({ hostname: "*" }),
 },
 source: "fetch-event",
 });
 e.waitUntil(self.skipWaiting());
});
addEventListener("activate", async (e) => {
 console.log(e.type);
 e.waitUntil(self.clients.claim());
});
addEventListener("message", async (e) => {
 console.log(e.type, e.data);
});
addEventListener("fetch", async (e) => {
 console.log(e);
 e.respondWith(fetch(e.request.url, {
 cache: "no-store",
 headers: {
 "pragma": "no-cache",
 "cache-control": "no-cache",
 "access-control-allow-origin": "*",
 },
 }));
});
chrome.windows.onRemoved.addListener((id) => {
 console.log(id);
 globalThis.nativeMessagingPort?.disconnect();
}, { windowTypes: ["app"] });
globalThis.ports = new Map([["web", null], ["iwa", null]]);
globalThis.nativeMessagingPort;
globalThis.iwaWindow;
globalThis.socketOptions;
globalThis.encoder = new TextEncoder();
function createTransformMessageStream() {
 globalThis.externalPromise = Promise.withResolvers();
 globalThis.transformStream = new TransformStream();
 globalThis.transformStream.readable.pipeTo(
 new WritableStream({
 async start() {
 console.log("start");
 return globalThis.externalPromise.promise;
 },
 write(data) {
 ports.get("iwa").postMessage(data);
 },
 close() {
 console.log("transformStreamWriter closed");
 },
 }),
 );
 globalThis.transformStreamWriter = globalThis.transformStream.writable
 .getWriter();
}
const IWA_BASE_URL = "isolated-app://zzz/";
chrome.runtime.onConnectExternal.addListener(async (port) => {
 if (
 !globalThis.nativeMessagingPort && !globalThis.iwaWindow &&
 port.name === "web"
 ) {
 globalThis.iwaWindow = await chrome.windows.create({
 url: IWA_BASE_URL,
 height: 200,
 width: 300,
 left: 0,
 top: 0,
 focused: true,
 type: "normal",
 });
 globalThis.nativeMessagingPort = chrome.runtime.connectNative(
 chrome.runtime.getManifest().short_name,
 );
 createTransformMessageStream();
 }
 console.log(port.name);
 if (port.name === "web") {
 ports.set("web", port);
 ports.get("web")
 .onMessage.addListener(async (message, p) => {
 await globalThis.transformStreamWriter.ready;
 await globalThis.transformStreamWriter.write(
 message,
 );
 });
 }
 if (port.name === "iwa") {
 ports.set("iwa", port);
 ports.get("iwa")
 .onMessage.addListener(async (message, p) => {
 ports.get("web").postMessage(message);
 });
 globalThis.externalPromise.resolve();
 }
 ports.set(port.name, port);
 ports.get(port.name)
 .onDisconnect.addListener(async ({ name }) => {
 console.log(`${name} disconnecting`);
 globalThis.nativeMessagingPort.disconnect();
 console.log(name + " disconnected");
 for (const [, p] of ports) {
 p.disconnect();
 }
 globalThis.transformStreamWriter.close()
 .catch(console.log);
 try {
 const tab = await chrome.tabs.query({
 title: "DirectSocket",
 });
 console.log(tab);
 if (tab.length) {
 await chrome.windows.remove(tab[0].windowId);
 }
 } catch (e) {
 console.log(e);
 } finally {
 globalThis.nativeMessagingPort = null;
 globalThis.iwaWindow = null;
 ports.clear();
 }
 });
 return true;
});
chrome.scripting.unregisterContentScripts().then(() =>
 chrome.scripting
 .registerContentScripts([{
 id: "sockets",
 js: ["direct-socket.js"],
 persistAcrossSessions: true,
 matches: [
 "https://*/*",
 "http://*/*",
 "*://zzz/*",
 ],
 runAt: "document_start",
 world: "MAIN",
 }])
).catch((e) => console.error(chrome.runtime.lastError, e));

The implementation, injected from the Web extension into Web pages

class DirectSocket {
 #port;
 #handleMessage;
 #handleDisconnect;
 #readableController;
 #readable = new ReadableStream({ 
 start: (_) => { 
 this.#readableController = _;
 } 
 });
 #delay = async (task = () => {}, delay = 50) => {
 return scheduler.postTask(task, {delay});
 };
 #writableController;
 #writable = new WritableStream({
 start: (_) => {
 this.#writableController = _;
 }, 
 write: async (message) => {
 await this.#delay();
 this.#port.postMessage(message?.data ? message : [...message]);
 },
 close: () => {
 try {
 this.#port.disconnect();
 this.#readableController.close();
 } catch (e) {
 console.log(e);
 }
 }
 });
 #socketParams = 0;
 #opened = Promise.withResolvers();
 #closed = Promise.withResolvers();
 closed;
 opened;
 constructor(socketType, remoteAddress, remotePort) {
 this.opened = this.#opened.promise;
 this.closed = this.#closed.promise;
 this.#handleMessage = (message, port) => {
 if (!(this.#socketParams)) {
 this.#socketParams = 1; 
 this.#opened.resolve({
 readable: this.#readable,
 writable: this.#writable,
 ...message
 });
 } else {
 this.#readableController.enqueue(Object.hasOwn(message, "data") ? { data: new Uint8Array(Object.values(message.data))} : new Uint8Array([...message]));
 }
 return true;
 }
 this.#handleDisconnect = (port) => {
 if (globalThis.chrome.runtime?.lastError) {
 console.log(globalThis.chrome.runtime.lastError);
 }
 this.#closed.resolve(void 0);
 }
 this.#port = globalThis.chrome.runtime.connect("belbbfgjhflfkchiobcofdndlflfedpg", 
 {name: "web"});
 this.#port.onMessage.addListener(this.#handleMessage);
 this.#port.onMessage.addListener(this.#handleDisconnect);
 this.#delay(() => {
 this.#port.postMessage({socketType, remoteAddress, remotePort});
 }, 100);
 }
 close() {
 this.#port.disconnect();
 }
}
if (!Object.hasOwn(globalThis, "DirectSocket")) {
 Object.assign(globalThis, {
 DirectSocket
 });
 console.log("DirectSocket declared");
}

Usage in arbitrary Web pages, primarily in Sources => Snippets

var socket = new DirectSocket("udp","0.0.0.0",10001);
var abortable = new AbortController();
var decoder = new TextDecoder();
var encoder = new TextEncoder();
var {readable, writable, remoteAddress, remotePort, localAddress, localPort} = await socket.opened;
console.log({
 remoteAddress,
 remotePort,
 localAddress,
 localPort,
 socket
});
Promise.allSettled([writable.closed, readable.closed]).then( (args, ) => console.log(args)).catch(console.error);
var reader = readable.getReader();
var writer = writable.getWriter();
async function stream(input) {
 let len = 65507;
 let bytes = 0;
 for (let i = 0; i < input.length; i += len) {
 await writer.ready;
 await writer.write({
 data: input.subarray(i, i + len)
 });
 var {value: {data}, done} = await reader.read();
 bytes += data.length;
 }
 return bytes;
}
// Echo 20 MB
var binaryResult = await stream(new Uint8Array(1024 ** 2 * 20)).catch( (e) => e);
// var textResult = await stream("text").catch((e) => e);
console.log({
 binaryResult,
 // textResult,
});
asked Aug 31 at 18:48
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
  • Your logging to console should be in a function with a log level. The final code should not write that much to the console.

  • Setting nuls looks like copy-pasted code, you could consider encapsulating in a helper function once.

  • document.body.insertAdjacentHTML( does not belong there. Full MVC might be overkill, but would rethink putting this in 'addListener'

  • close() { console.log("close"); }, , this does not look like functioning code ;)

  • I might be missing something, but using ports.get("web").postMessage(message); inside the iwa logic is either wrong or merits a comment why it's not wrong

answered Sep 2 at 9:40
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.