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,
});
1 Answer 1
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 theiwa
logic is either wrong or merits a comment why it's not wrong