sandstone-proxy
Version:
An experimental web proxy utilizing sandboxed iframes and no service worker.
125 lines (105 loc) • 3.46 kB
JavaScript
import { iframes } from "./controller.mjs";
import { rpc_handlers } from "../rpc.mjs";
import { libcurl } from "libcurl.js/bundled";
export const ws_connections = {};
export let session = null;
export function set_websocket(url) {
libcurl.set_websocket(url);
}
function get_ws(frame_id, ws_id) {
let frame_websockets = ws_connections[frame_id];
if (!frame_websockets) return;
return frame_websockets[ws_id];
}
//handle fetch api requests
rpc_handlers["fetch"] = async function(url, options) {
var response = await session.fetch(url, options);
var keys = ["ok", "redirected", "status", "statusText", "type", "url", "raw_headers"];
var payload = {
body: await response.blob(),
headers: [],
items: {}
};
if (payload.body.type.includes(";")) {
let mime_type = payload.body.type.split(";")[0].trim();
payload.body = new Blob([payload.body], {type: mime_type});
}
for (let key of keys) {
payload.items[key] = response[key];
}
for (let pair of response.headers.entries()) {
payload.headers.push(pair);
}
return payload
}
//handle websocket creation
rpc_handlers["ws_new"] = function (frame_id, url, protocols, options) {
let ws_id = Math.random() + "";
let ws = new libcurl.CurlWebSocket(url, protocols, options);
let ws_events = [];
if (!ws_connections[frame_id]) ws_connections[frame_id] = {};
ws_connections[frame_id][ws_id] = {
ws: ws,
events: ws_events,
callback: null
};
let ws_info = ws_connections[frame_id][ws_id];
//set up event listeners to forward to the frame
for (let event_name of ["open", "message", "close", "error"]) {
ws["on" + event_name] = (data) => {
ws_info.events.push([event_name, data]);
ws_info.callback?.();
}
}
//make sure ws is closed automatically
let close_callback = ws.onclose;
ws.onclose = (reason) => {
close_callback(reason);
ws.close();
}
return ws_id;
}
//the frame will call this repeatedly to poll for new events
rpc_handlers["ws_event"] = function (frame_id, ws_id) {
let ws_info = get_ws(frame_id, ws_id);
if (!ws_info) return null;
if (ws_info.events.length > 0) {
let ws_events = ws_info.events;
ws_info.events = [];
return ws_events;
}
return new Promise((resolve) => {
ws_info.callback = () => {
resolve(ws_info.events);
ws_info.events = [];
ws_info.callback = null;
}
})
}
rpc_handlers["ws_send"] = function (frame_id, ws_id, data) {
let ws_info = get_ws(frame_id, ws_id);
if (!ws_info) return;
ws_info.ws.send(data);
}
rpc_handlers["ws_close"] = function (frame_id, ws_id) {
let ws_info = get_ws(frame_id, ws_id);
if (!ws_info) return;
delete ws_connections[frame_id][ws_id];
ws_info.ws.close();
}
//when navigating to a new page we need to close unused connections
export function clean_ws_connections(id_to_clean) {
let frame_ids = Object.keys(iframes);
for (let [frame_id, frame_websockets] of Object.entries(ws_connections)) {
if (frame_ids.includes(frame_id) && frame_id !== id_to_clean) continue;
for (let [ws_id, ws_info] of Object.entries(frame_websockets)) {
delete ws_connections[frame_id][ws_id];
ws_info.ws.close();
}
delete ws_connections[frame_id];
}
}
libcurl.events.addEventListener("libcurl_load", () => {
console.log(`libcurl.js v${libcurl.version.lib} loaded`);
session = new libcurl.HTTPSession({enable_cookies: true})
});