support url base

This commit is contained in:
velzie 2024-09-01 13:26:24 -04:00
parent a1ce4e33b3
commit 7a9c990b01
No known key found for this signature in database
GPG key ID: 048413F95F0DDE1F
32 changed files with 213 additions and 158 deletions

View file

@ -15,6 +15,7 @@ import {
} from "../shared"; } from "../shared";
import { createWrapFn } from "./shared/wrap"; import { createWrapFn } from "./shared/wrap";
import { NavigateEvent } from "./events"; import { NavigateEvent } from "./events";
import type { URLMeta } from "../shared/rewriters/url";
declare global { declare global {
interface Window { interface Window {
@ -78,6 +79,8 @@ export class ScramjetClient {
] ]
> = new Map(); > = new Map();
meta: URLMeta;
constructor(public global: typeof globalThis) { constructor(public global: typeof globalThis) {
this.serviceWorker = this.global.navigator.serviceWorker; this.serviceWorker = this.global.navigator.serviceWorker;
@ -103,6 +106,27 @@ export class ScramjetClient {
}) })
); );
} }
let baseurl: URL;
if (iswindow) {
// setup base url
// base url can only be updated at document load time and it will affect all urls resolved by encodeurl/rewriteurl
const base = this.global.document.querySelector("base");
if (base) {
baseurl = new URL(decodeUrl(base.href));
}
}
const client = this;
this.meta = {
get origin() {
return client.url;
},
get base() {
return baseurl || client.url;
},
};
global[SCRAMJETCLIENT] = this; global[SCRAMJETCLIENT] = this;
} }
@ -169,7 +193,7 @@ export class ScramjetClient {
} }
if (ev.defaultPrevented) return; if (ev.defaultPrevented) return;
self.location.href = encodeUrl(ev.url); self.location.href = encodeUrl(ev.url, this.meta);
} }
// below are the utilities for proxying and trapping dom APIs // below are the utilities for proxying and trapping dom APIs

View file

@ -21,7 +21,7 @@ export function createDocumentProxy(
}, },
set(target, prop, newValue) { set(target, prop, newValue) {
if (prop === "location") { if (prop === "location") {
location.href = encodeUrl(newValue); location.href = encodeUrl(newValue, client.meta);
return; return;
} }

View file

@ -18,7 +18,7 @@ export default function (client: ScramjetClient) {
client.Proxy("CSSStyleDeclaration.prototype.setProperty", { client.Proxy("CSSStyleDeclaration.prototype.setProperty", {
apply(ctx) { apply(ctx) {
if (cssProperties.includes(ctx.args[0])) if (cssProperties.includes(ctx.args[0]))
ctx.args[1] = rewriteCss(ctx.args[1]); ctx.args[1] = rewriteCss(ctx.args[1], client.meta);
}, },
}); });
} }

View file

@ -63,11 +63,11 @@ export default function (client: ScramjetClient, self: typeof window) {
} else if ( } else if (
["src", "data", "href", "action", "formaction"].includes(attr) ["src", "data", "href", "action", "formaction"].includes(attr)
) { ) {
value = encodeUrl(value); value = encodeUrl(value, client.meta);
} else if (attr === "srcdoc") { } else if (attr === "srcdoc") {
value = rewriteHtml(value, client.cookieStore, undefined, true); value = rewriteHtml(value, client.cookieStore, undefined, true);
} else if (["srcset", "imagesrcset"].includes(attr)) { } else if (["srcset", "imagesrcset"].includes(attr)) {
value = rewriteSrcset(value); value = rewriteSrcset(value, client.meta);
} }
descriptor.set.call(this, value); descriptor.set.call(this, value);
@ -106,6 +106,7 @@ export default function (client: ScramjetClient, self: typeof window) {
client.Trap("Node.prototype.baseURI", { client.Trap("Node.prototype.baseURI", {
get() { get() {
// TODO this should be using ownerdocument but who gaf
const base = self.document.querySelector("base"); const base = self.document.querySelector("base");
if (base) { if (base) {
return new URL(base.href, client.url).href; return new URL(base.href, client.url).href;
@ -132,7 +133,7 @@ export default function (client: ScramjetClient, self: typeof window) {
}); });
if (rule) { if (rule) {
ctx.args[1] = rule.fn(value, client.url, client.cookieStore); ctx.args[1] = rule.fn(value, client.meta, client.cookieStore);
ctx.fn.call(ctx.this, `data-scramjet-${ctx.args[0]}`, value); ctx.fn.call(ctx.this, `data-scramjet-${ctx.args[0]}`, value);
} }
}, },
@ -152,11 +153,11 @@ export default function (client: ScramjetClient, self: typeof window) {
set(ctx, value: string) { set(ctx, value: string) {
let newval; let newval;
if (ctx.this instanceof self.HTMLScriptElement) { if (ctx.this instanceof self.HTMLScriptElement) {
newval = rewriteJs(value, client.url); newval = rewriteJs(value, client.meta);
} else if (ctx.this instanceof self.HTMLStyleElement) { } else if (ctx.this instanceof self.HTMLStyleElement) {
newval = rewriteCss(value, client.url); newval = rewriteCss(value, client.meta);
} else { } else {
newval = rewriteHtml(value, client.cookieStore, client.url); newval = rewriteHtml(value, client.cookieStore, client.meta);
} }
ctx.set(newval); ctx.set(newval);
@ -168,7 +169,7 @@ export default function (client: ScramjetClient, self: typeof window) {
client.Trap("Element.prototype.outerHTML", { client.Trap("Element.prototype.outerHTML", {
set(ctx, value: string) { set(ctx, value: string) {
ctx.set(rewriteHtml(value, client.cookieStore, client.url)); ctx.set(rewriteHtml(value, client.cookieStore, client.meta));
}, },
get(ctx) { get(ctx) {
return unrewriteHtml(ctx.get()); return unrewriteHtml(ctx.get());

View file

@ -4,7 +4,7 @@ import { decodeUrl, rewriteCss } from "../../shared";
export default function (client: ScramjetClient, self: typeof window) { export default function (client: ScramjetClient, self: typeof window) {
client.Proxy("FontFace", { client.Proxy("FontFace", {
construct(ctx) { construct(ctx) {
ctx.args[1] = rewriteCss(ctx.args[1]); ctx.args[1] = rewriteCss(ctx.args[1], client.meta);
}, },
}); });
} }

View file

@ -5,7 +5,7 @@ import { UrlChangeEvent } from "../events";
export default function (client: ScramjetClient, self: typeof globalThis) { export default function (client: ScramjetClient, self: typeof globalThis) {
client.Proxy("history.pushState", { client.Proxy("history.pushState", {
apply(ctx) { apply(ctx) {
ctx.args[2] = encodeUrl(ctx.args[2]); ctx.args[2] = encodeUrl(ctx.args[2], client.meta);
ctx.call(); ctx.call();
const ev = new UrlChangeEvent(client.url.href); const ev = new UrlChangeEvent(client.url.href);
@ -15,7 +15,7 @@ export default function (client: ScramjetClient, self: typeof globalThis) {
client.Proxy("history.replaceState", { client.Proxy("history.replaceState", {
apply(ctx) { apply(ctx) {
ctx.args[2] = encodeUrl(ctx.args[2]); ctx.args[2] = encodeUrl(ctx.args[2], client.meta);
ctx.call(); ctx.call();
const ev = new UrlChangeEvent(client.url.href); const ev = new UrlChangeEvent(client.url.href);

View file

@ -5,7 +5,7 @@ import { SCRAMJETCLIENT } from "../../symbols";
export default function (client: ScramjetClient) { export default function (client: ScramjetClient) {
client.Proxy("window.open", { client.Proxy("window.open", {
apply(ctx) { apply(ctx) {
if (ctx.args[0]) ctx.args[0] = encodeUrl(ctx.args[0]); if (ctx.args[0]) ctx.args[0] = encodeUrl(ctx.args[0], client.meta);
if (["_parent", "_top", "_unfencedTop"].includes(ctx.args[1])) if (["_parent", "_top", "_unfencedTop"].includes(ctx.args[1]))
ctx.args[1] = "_self"; ctx.args[1] = "_self";

View file

@ -4,6 +4,7 @@ import { decodeUrl } from "../../shared";
export default function (client: ScramjetClient, self: typeof window) { export default function (client: ScramjetClient, self: typeof window) {
client.Trap("origin", { client.Trap("origin", {
get() { get() {
// this isn't right!!
return client.url.origin; return client.url.origin;
}, },
set() { set() {

View file

@ -16,7 +16,7 @@ export default function (client: ScramjetClient, self: Self) {
client.Proxy("Worklet.prototype.addModule", { client.Proxy("Worklet.prototype.addModule", {
apply(ctx) { apply(ctx) {
ctx.args[0] = encodeUrl(ctx.args[0]); ctx.args[0] = encodeUrl(ctx.args[0], client.meta);
}, },
}); });
@ -65,7 +65,7 @@ export default function (client: ScramjetClient, self: Self) {
client.Proxy("navigator.serviceWorker.register", { client.Proxy("navigator.serviceWorker.register", {
apply(ctx) { apply(ctx) {
if (ctx.args[0] instanceof URL) ctx.args[0] = ctx.args[0].href; if (ctx.args[0] instanceof URL) ctx.args[0] = ctx.args[0].href;
let url = encodeUrl(ctx.args[0]) + "?dest=serviceworker"; let url = encodeUrl(ctx.args[0], client.meta) + "?dest=serviceworker";
if (ctx.args[1] && ctx.args[1].type === "module") { if (ctx.args[1] && ctx.args[1].type === "module") {
url += "&type=module"; url += "&type=module";
} }

View file

@ -43,7 +43,7 @@ export default function (client: ScramjetClient, self: typeof window) {
).length; ).length;
default: default:
if (prop in Object.prototype) { if (prop in Object.prototype || typeof prop === "symbol") {
return Reflect.get(target, prop); return Reflect.get(target, prop);
} }
console.log("GET", prop, target == realLocalStorage); console.log("GET", prop, target == realLocalStorage);

View file

@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import { ScramjetClient } from "./client"; import { ScramjetClient } from "./client";
import { nativeGetOwnPropertyDescriptor } from "./natives"; import { nativeGetOwnPropertyDescriptor } from "./natives";
import { encodeUrl, decodeUrl } from "../shared"; import { decodeUrl, encodeUrl } from "../shared";
import { iswindow } from "."; import { iswindow } from ".";
export function createLocationProxy( export function createLocationProxy(
@ -76,7 +76,7 @@ export function createLocationProxy(
if (self.location.assign) if (self.location.assign)
fakeLocation.assign = new Proxy(self.location.assign, { fakeLocation.assign = new Proxy(self.location.assign, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
args[0] = encodeUrl(args[0]); args[0] = encodeUrl(args[0], client.meta);
Reflect.apply(target, self.location, args); Reflect.apply(target, self.location, args);
}, },
}); });
@ -89,7 +89,7 @@ export function createLocationProxy(
if (self.location.replace) if (self.location.replace)
fakeLocation.replace = new Proxy(self.location.replace, { fakeLocation.replace = new Proxy(self.location.replace, {
apply(target, thisArg, args) { apply(target, thisArg, args) {
args[0] = encodeUrl(args[0]); args[0] = encodeUrl(args[0], client.meta);
Reflect.apply(target, self.location, args); Reflect.apply(target, self.location, args);
}, },
}); });

View file

@ -2,25 +2,34 @@ import { config } from "../../shared";
import { ScramjetClient } from "../client"; import { ScramjetClient } from "../client";
export const enabled = () => config.flags.captureErrors; export const enabled = () => config.flags.captureErrors;
export function argdbg(arg, recurse = []) {
export default function (client: ScramjetClient, self: typeof globalThis) {
function argdbg(arg) {
switch (typeof arg) { switch (typeof arg) {
case "string": case "string":
if (arg.includes("scramjet") && !arg.includes("\n")) debugger; if (arg.includes("localhost:1337/scramjet/") && arg.includes("m3u8"))
debugger;
break; break;
case "object": case "object":
if (arg instanceof Location) debugger; // if (arg instanceof Location) debugger;
if ( if (
arg && arg &&
arg[Symbol.iterator] && arg[Symbol.iterator] &&
typeof arg[Symbol.iterator] === "function" typeof arg[Symbol.iterator] === "function"
) )
for (let ar of arg) argdbg(ar); for (let prop in arg) {
// make sure it's not a getter
let desc = Object.getOwnPropertyDescriptor(arg, prop);
if (desc && desc.get) continue;
const ar = arg[prop];
if (recurse.includes(ar)) continue;
recurse.push(ar);
argdbg(ar, recurse);
}
break; break;
} }
} }
export default function (client: ScramjetClient, self: typeof globalThis) {
self.$scramerr = function scramerr(e) { self.$scramerr = function scramerr(e) {
console.warn("CAUGHT ERROR", e); console.warn("CAUGHT ERROR", e);
}; };

View file

@ -8,7 +8,7 @@ export default function (client: ScramjetClient, self: Self) {
value: function (js: any) { value: function (js: any) {
if (typeof js !== "string") return js; if (typeof js !== "string") return js;
const rewritten = rewriteJs(js, client.url); const rewritten = rewriteJs(js, client.meta);
return rewritten; return rewritten;
}, },
@ -23,5 +23,5 @@ export function indirectEval(this: ScramjetClient, js: any) {
const indirection = this.global.eval; const indirection = this.global.eval;
return indirection(rewriteJs(js, this.url) as string); return indirection(rewriteJs(js, this.meta) as string);
} }

View file

@ -1,19 +1,19 @@
import { ScramjetClient, ProxyCtx, Proxy } from "../client"; import { ScramjetClient, ProxyCtx, Proxy } from "../client";
import { rewriteJs } from "../../shared"; import { rewriteJs } from "../../shared";
function rewriteFunction(ctx: ProxyCtx) { function rewriteFunction(ctx: ProxyCtx, client: ScramjetClient) {
const stringifiedFunction = ctx.call().toString(); const stringifiedFunction = ctx.call().toString();
ctx.return(ctx.fn(`return ${rewriteJs(stringifiedFunction)}`)()); ctx.return(ctx.fn(`return ${rewriteJs(stringifiedFunction, client.meta)}`)());
} }
export default function (client: ScramjetClient, self: Self) { export default function (client: ScramjetClient, self: Self) {
const handler: Proxy = { const handler: Proxy = {
apply(ctx) { apply(ctx) {
rewriteFunction(ctx); rewriteFunction(ctx, client);
}, },
construct(ctx) { construct(ctx) {
rewriteFunction(ctx); rewriteFunction(ctx, client);
}, },
}; };

View file

@ -1,5 +1,6 @@
import { ScramjetClient } from "../client"; import { ScramjetClient } from "../client";
import { config, encodeUrl } from "../../shared"; import { config } from "../../shared";
import { encodeUrl } from "../../shared/rewriters/url";
export default function (client: ScramjetClient, self: Self) { export default function (client: ScramjetClient, self: Self) {
const Function = client.natives.Function; const Function = client.natives.Function;
@ -8,7 +9,7 @@ export default function (client: ScramjetClient, self: Self) {
return function (url: string) { return function (url: string) {
const resolved = new URL(url, base).href; const resolved = new URL(url, base).href;
return Function(`return import("${encodeUrl(resolved)}")`)(); return Function(`return import("${encodeUrl(resolved, client.meta)}")`)();
}; };
}; };

View file

@ -9,7 +9,7 @@ export default function (client: ScramjetClient, self: typeof globalThis) {
client.Proxy("fetch", { client.Proxy("fetch", {
apply(ctx) { apply(ctx) {
if (typeof ctx.args[0] === "string" || ctx.args[0] instanceof URL) { if (typeof ctx.args[0] === "string" || ctx.args[0] instanceof URL) {
ctx.args[0] = encodeUrl(ctx.args[0].toString()); ctx.args[0] = encodeUrl(ctx.args[0].toString(), client.meta);
if (isemulatedsw) ctx.args[0] += "?from=swruntime"; if (isemulatedsw) ctx.args[0] += "?from=swruntime";
} }
@ -25,7 +25,7 @@ export default function (client: ScramjetClient, self: typeof globalThis) {
client.Proxy("Request", { client.Proxy("Request", {
construct(ctx) { construct(ctx) {
if (typeof ctx.args[0] === "string" || ctx.args[0] instanceof URL) { if (typeof ctx.args[0] === "string" || ctx.args[0] instanceof URL) {
ctx.args[0] = encodeUrl(ctx.args[0].toString()); ctx.args[0] = encodeUrl(ctx.args[0].toString(), client.meta);
if (isemulatedsw) ctx.args[0] += "?from=swruntime"; if (isemulatedsw) ctx.args[0] += "?from=swruntime";
} }

View file

@ -1,16 +1,17 @@
import { decodeUrl, encodeUrl, rewriteHeaders } from "../../../shared"; import { decodeUrl, encodeUrl, rewriteHeaders } from "../../../shared";
import { ScramjetClient } from "../../client";
export default function (client, self) { export default function (client: ScramjetClient, self: Self) {
client.Proxy("XMLHttpRequest.prototype.open", { client.Proxy("XMLHttpRequest.prototype.open", {
apply(ctx) { apply(ctx) {
if (ctx.args[1]) ctx.args[1] = encodeUrl(ctx.args[1]); if (ctx.args[1]) ctx.args[1] = encodeUrl(ctx.args[1], client.meta);
}, },
}); });
client.Proxy("XMLHttpRequest.prototype.setRequestHeader", { client.Proxy("XMLHttpRequest.prototype.setRequestHeader", {
apply(ctx) { apply(ctx) {
let headerObject = Object.fromEntries([ctx.args]); let headerObject = Object.fromEntries([ctx.args]);
headerObject = rewriteHeaders(headerObject); headerObject = rewriteHeaders(headerObject, client.meta);
ctx.args = Object.entries(headerObject)[0]; ctx.args = Object.entries(headerObject)[0];
}, },
@ -18,7 +19,7 @@ export default function (client, self) {
client.Trap("XMLHttpRequest.prototype.responseURL", { client.Trap("XMLHttpRequest.prototype.responseURL", {
get(ctx) { get(ctx) {
return decodeUrl(ctx.get()); return decodeUrl(ctx.get() as string);
}, },
}); });
} }

View file

@ -1,4 +1,5 @@
import { encodeUrl, BareMuxConnection } from "../../shared"; import { BareMuxConnection } from "../../shared";
import { encodeUrl } from "../../shared/rewriters/url";
import type { MessageC2W } from "../../worker"; import type { MessageC2W } from "../../worker";
import { ScramjetClient } from "../client"; import { ScramjetClient } from "../client";
@ -23,7 +24,7 @@ export default function (client: ScramjetClient, self: typeof globalThis) {
id, id,
} as MessageC2W); } as MessageC2W);
} else { } else {
args[0] = encodeUrl(args[0]) + "?dest=worker"; args[0] = encodeUrl(args[0], client.meta) + "?dest=worker";
if (args[1] && args[1].type === "module") { if (args[1] && args[1].type === "module") {
args[0] += "&type=module"; args[0] += "&type=module";

View file

@ -2,6 +2,7 @@ import { iswindow, isworker } from "..";
import { SCRAMJETCLIENT } from "../../symbols"; import { SCRAMJETCLIENT } from "../../symbols";
import { ScramjetClient } from "../client"; import { ScramjetClient } from "../client";
import { config } from "../../shared"; import { config } from "../../shared";
import { argdbg } from "./err";
export function createWrapFn(client: ScramjetClient, self: typeof globalThis) { export function createWrapFn(client: ScramjetClient, self: typeof globalThis) {
return function (identifier: any, args: any) { return function (identifier: any, args: any) {

View file

@ -1,5 +1,5 @@
import { ScramjetClient } from "./client"; import { ScramjetClient } from "./client";
import { decodeUrl, encodeUrl } from "../shared"; import { decodeUrl } from "../shared";
export class ScramjetServiceWorkerRuntime { export class ScramjetServiceWorkerRuntime {
recvport: MessagePort; recvport: MessagePort;

View file

@ -1,10 +1,11 @@
import { encodeUrl } from "../../shared"; import { encodeUrl } from "../../shared";
import { ScramjetClient } from "../client";
export default function (client, self) { export default function (client: ScramjetClient, self: Self) {
client.Proxy("importScripts", { client.Proxy("importScripts", {
apply(ctx) { apply(ctx) {
for (const i in ctx.args) { for (const i in ctx.args) {
ctx.args[i] = encodeUrl(ctx.args[i]); ctx.args[i] = encodeUrl(ctx.args[i], client.meta);
} }
}, },
}); });

View file

@ -26,7 +26,7 @@ export class ScramjetController {
client: "/scramjet.client.js", client: "/scramjet.client.js",
codecs: "/scramjet.codecs.js", codecs: "/scramjet.codecs.js",
flags: { flags: {
serviceworkers: true, serviceworkers: false,
naiiveRewriter: false, naiiveRewriter: false,
captureErrors: false, captureErrors: false,
}, },

View file

@ -1,3 +1,4 @@
// thnank you node unblocker guy
import parse from "set-cookie-parser"; import parse from "set-cookie-parser";
export type Cookie = { export type Cookie = {

View file

@ -1,9 +1,9 @@
// This CSS rewriter uses code from Meteor // This CSS rewriter uses code from Meteor
// You can find the original source code at https://github.com/MeteorProxy/Meteor // You can find the original source code at https://github.com/MeteorProxy/Meteor
import { encodeUrl } from "./url"; import { URLMeta, encodeUrl } from "./url";
export function rewriteCss(css: string, origin?: URL) { export function rewriteCss(css: string, meta: URLMeta) {
const regex = const regex =
/(@import\s+(?!url\())?\s*url\(\s*(['"]?)([^'")]+)\2\s*\)|@import\s+(['"])([^'"]+)\4/g; /(@import\s+(?!url\())?\s*url\(\s*(['"]?)([^'")]+)\2\s*\)|@import\s+(['"])([^'"]+)\4/g;
@ -18,7 +18,7 @@ export function rewriteCss(css: string, origin?: URL) {
importContent importContent
) => { ) => {
const url = urlContent || importContent; const url = urlContent || importContent;
const encodedUrl = encodeUrl(url.trim(), origin); const encodedUrl = encodeUrl(url.trim(), meta);
if (importStatement) { if (importStatement) {
return `@import url(${urlQuote}${encodedUrl}${urlQuote})`; return `@import url(${urlQuote}${encodedUrl}${urlQuote})`;

View file

@ -1,4 +1,6 @@
import { encodeUrl } from "./url"; // TODO this whole file should be inlined and deleted it's a weird relic from ssd era
import { URLMeta, encodeUrl } from "./url";
import { BareHeaders } from "@mercuryworkshop/bare-mux"; import { BareHeaders } from "@mercuryworkshop/bare-mux";
const cspHeaders = [ const cspHeaders = [
"cross-origin-embedder-policy", "cross-origin-embedder-policy",
@ -24,11 +26,11 @@ const cspHeaders = [
const urlHeaders = ["location", "content-location", "referer"]; const urlHeaders = ["location", "content-location", "referer"];
function rewriteLinkHeader(link: string, origin?: URL) { function rewriteLinkHeader(link: string, meta: URLMeta) {
return link.replace(/<(.*)>/gi, (match) => encodeUrl(match, origin)); return link.replace(/<(.*)>/gi, (match) => encodeUrl(match, meta));
} }
export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) { export function rewriteHeaders(rawHeaders: BareHeaders, meta: URLMeta) {
const headers = {}; const headers = {};
for (const key in rawHeaders) { for (const key in rawHeaders) {
@ -41,17 +43,14 @@ export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) {
urlHeaders.forEach((header) => { urlHeaders.forEach((header) => {
if (headers[header]) if (headers[header])
headers[header] = encodeUrl( headers[header] = encodeUrl(headers[header]?.toString() as string, meta);
headers[header]?.toString() as string,
origin
);
}); });
if (typeof headers["link"] === "string") { if (typeof headers["link"] === "string") {
headers["link"] = rewriteLinkHeader(headers["link"], origin); headers["link"] = rewriteLinkHeader(headers["link"], meta);
} else if (Array.isArray(headers["link"])) { } else if (Array.isArray(headers["link"])) {
headers["link"] = headers["link"].map((link) => headers["link"] = headers["link"].map((link) =>
rewriteLinkHeader(link, origin) rewriteLinkHeader(link, meta)
); );
} }

View file

@ -1,7 +1,7 @@
import { ElementType, Parser } from "htmlparser2"; import { ElementType, Parser } from "htmlparser2";
import { ChildNode, DomHandler, Element, Node, Text } from "domhandler"; import { ChildNode, DomHandler, Element, Node, Text } from "domhandler";
import render from "dom-serializer"; import render from "dom-serializer";
import { encodeUrl } from "./url"; import { URLMeta, encodeUrl } from "./url";
import { rewriteCss } from "./css"; import { rewriteCss } from "./css";
import { rewriteJs } from "./js"; import { rewriteJs } from "./js";
import { CookieStore } from "../cookie"; import { CookieStore } from "../cookie";
@ -9,7 +9,7 @@ import { CookieStore } from "../cookie";
export function rewriteHtml( export function rewriteHtml(
html: string, html: string,
cookieStore: CookieStore, cookieStore: CookieStore,
origin?: URL, meta: URLMeta,
fromTop: boolean = false fromTop: boolean = false
) { ) {
const handler = new DomHandler((err, dom) => dom); const handler = new DomHandler((err, dom) => dom);
@ -17,7 +17,7 @@ export function rewriteHtml(
parser.write(html); parser.write(html);
parser.end(); parser.end();
traverseParsedHtml(handler.root, cookieStore, origin); traverseParsedHtml(handler.root, cookieStore, meta);
function findhead(node) { function findhead(node) {
if (node.type === ElementType.Tag && node.name === "head") { if (node.type === ElementType.Tag && node.name === "head") {
@ -62,6 +62,11 @@ export function rewriteHtml(
return render(handler.root); return render(handler.root);
} }
type ParseState = {
base: string;
origin?: URL;
};
export function unrewriteHtml(html: string) { export function unrewriteHtml(html: string) {
const handler = new DomHandler((err, dom) => dom); const handler = new DomHandler((err, dom) => dom);
const parser = new Parser(handler); const parser = new Parser(handler);
@ -93,17 +98,11 @@ export function unrewriteHtml(html: string) {
export const htmlRules: { export const htmlRules: {
[key: string]: "*" | string[] | Function; [key: string]: "*" | string[] | Function;
fn: ( fn: (value: string, meta: URLMeta, cookieStore: CookieStore) => string | null;
value: string,
origin: URL | null,
cookieStore: CookieStore
) => string | null;
}[] = [ }[] = [
{ {
fn: (value: string, origin: URL) => { fn: (value: string, meta: URLMeta) => {
if (["_parent", "_top", "_unfencedTop"].includes(value)) return "_self"; return encodeUrl(value, meta);
return encodeUrl(value, origin);
}, },
// url rewrites // url rewrites
@ -133,34 +132,51 @@ export const htmlRules: {
csp: ["iframe"], csp: ["iframe"],
}, },
{ {
fn: (value: string, origin?: URL) => rewriteSrcset(value, origin), fn: (value: string, meta: URLMeta) => rewriteSrcset(value, meta),
// srcset // srcset
srcset: ["img", "source"], srcset: ["img", "source"],
imagesrcset: ["link"], imagesrcset: ["link"],
}, },
{ {
fn: (value: string, origin: URL, cookieStore: CookieStore) => fn: (value: string, meta: URLMeta, cookieStore: CookieStore) =>
rewriteHtml(value, cookieStore, origin, true), rewriteHtml(
value,
cookieStore,
{
// for srcdoc origin is the origin of the page that the iframe is on. base and path get dropped
origin: new URL(meta.origin.origin),
base: new URL(meta.origin.origin),
},
true
),
// srcdoc // srcdoc
srcdoc: ["iframe"], srcdoc: ["iframe"],
}, },
{ {
fn: (value: string, origin?: URL) => rewriteCss(value, origin), fn: (value: string, meta: URLMeta) => rewriteCss(value, meta),
style: "*", style: "*",
}, },
{ {
fn: (value: string) => { fn: (value: string) => {
if (["_parent", "_top", "_unfencedTop"].includes(value)) return "_self"; if (["_parent", "_top", "_unfencedTop"].includes(value)) return "_self";
}, },
target: ["a"], target: ["a", "base"],
}, },
]; ];
// i need to add the attributes in during rewriting // i need to add the attributes in during rewriting
function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) { function traverseParsedHtml(
node: any,
cookieStore: CookieStore,
meta: URLMeta
) {
if (node.name === "base" && node.attribs.href !== undefined) {
meta.base = new URL(node.attribs.href, meta.origin);
}
if (node.attribs) if (node.attribs)
for (const rule of htmlRules) { for (const rule of htmlRules) {
for (const attr in rule) { for (const attr in rule) {
@ -170,7 +186,7 @@ function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) {
if (sel === "*" || sel.includes(node.name)) { if (sel === "*" || sel.includes(node.name)) {
if (node.attribs[attr] !== undefined) { if (node.attribs[attr] !== undefined) {
const value = node.attribs[attr]; const value = node.attribs[attr];
let v = rule.fn(value, origin, cookieStore); let v = rule.fn(value, meta, cookieStore);
if (v === null) delete node.attribs[attr]; if (v === null) delete node.attribs[attr];
else { else {
@ -183,7 +199,7 @@ function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) {
} }
if (node.name === "style" && node.children[0] !== undefined) if (node.name === "style" && node.children[0] !== undefined)
node.children[0].data = rewriteCss(node.children[0].data, origin); node.children[0].data = rewriteCss(node.children[0].data, meta);
if ( if (
node.name === "script" && node.name === "script" &&
@ -195,7 +211,7 @@ function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) {
let js = node.children[0].data; let js = node.children[0].data;
const htmlcomment = /<!--[\s\S]*?-->/g; const htmlcomment = /<!--[\s\S]*?-->/g;
js = js.replace(htmlcomment, ""); js = js.replace(htmlcomment, "");
node.children[0].data = rewriteJs(js, origin); node.children[0].data = rewriteJs(js, meta);
} }
if (node.name === "meta" && node.attribs["http-equiv"] != undefined) { if (node.name === "meta" && node.attribs["http-equiv"] != undefined) {
@ -209,7 +225,7 @@ function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) {
) { ) {
const contentArray = node.attribs.content.split("url="); const contentArray = node.attribs.content.split("url=");
if (contentArray[1]) if (contentArray[1])
contentArray[1] = encodeUrl(contentArray[1].trim(), origin); contentArray[1] = encodeUrl(contentArray[1].trim(), meta);
node.attribs.content = contentArray.join("url="); node.attribs.content = contentArray.join("url=");
} }
} }
@ -219,7 +235,7 @@ function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) {
node.childNodes[childNode] = traverseParsedHtml( node.childNodes[childNode] = traverseParsedHtml(
node.childNodes[childNode], node.childNodes[childNode],
cookieStore, cookieStore,
origin meta
); );
} }
} }
@ -227,14 +243,14 @@ function traverseParsedHtml(node: any, cookieStore: CookieStore, origin?: URL) {
return node; return node;
} }
export function rewriteSrcset(srcset: string, origin?: URL) { export function rewriteSrcset(srcset: string, meta: URLMeta) {
const urls = srcset.split(/ [0-9]+x,? ?/g); const urls = srcset.split(/ [0-9]+x,? ?/g);
if (!urls) return ""; if (!urls) return "";
const sufixes = srcset.match(/ [0-9]+x,? ?/g); const sufixes = srcset.match(/ [0-9]+x,? ?/g);
if (!sufixes) return ""; if (!sufixes) return "";
const rewrittenUrls = urls.map((url, i) => { const rewrittenUrls = urls.map((url, i) => {
if (url && sufixes[i]) { if (url && sufixes[i]) {
return encodeUrl(url, origin) + sufixes[i]; return encodeUrl(url, meta) + sufixes[i];
} }
}); });

View file

@ -1,4 +1,4 @@
import { decodeUrl } from "./url"; import { URLMeta, decodeUrl } from "./url";
// i am a cat. i like to be petted. i like to be fed. i like to be // i am a cat. i like to be petted. i like to be fed. i like to be
import { import {
@ -18,25 +18,21 @@ init();
Error.stackTraceLimit = 50; Error.stackTraceLimit = 50;
global.rws = rewriteJs; export function rewriteJs(js: string | ArrayBuffer, meta: URLMeta) {
export function rewriteJs(js: string | ArrayBuffer, origin?: URL) {
if ("window" in globalThis)
origin = origin ?? new URL(decodeUrl(location.href));
if (self.$scramjet.config.flags.naiiveRewriter) { if (self.$scramjet.config.flags.naiiveRewriter) {
const text = typeof js === "string" ? js : new TextDecoder().decode(js); const text = typeof js === "string" ? js : new TextDecoder().decode(js);
return rewriteJsNaiive(text, origin); return rewriteJsNaiive(text);
} }
const before = performance.now(); const before = performance.now();
if (typeof js === "string") { if (typeof js === "string") {
js = new TextDecoder().decode( js = new TextDecoder().decode(
rewrite_js(js, origin.toString(), self.$scramjet) rewrite_js(js, meta.base.href, self.$scramjet)
); );
} else { } else {
js = rewrite_js_from_arraybuffer( js = rewrite_js_from_arraybuffer(
new Uint8Array(js), new Uint8Array(js),
origin.toString(), meta.base.href,
self.$scramjet self.$scramjet
); );
} }
@ -53,10 +49,7 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) {
// 4. i think the global state can get clobbered somehow // 4. i think the global state can get clobbered somehow
// //
// if you can ensure all the preconditions are met this is faster than full rewrites // if you can ensure all the preconditions are met this is faster than full rewrites
export function rewriteJsNaiive(js: string | ArrayBuffer, origin?: URL) { export function rewriteJsNaiive(js: string | ArrayBuffer) {
if ("window" in globalThis)
origin = origin ?? new URL(decodeUrl(location.href));
if (typeof js !== "string") { if (typeof js !== "string") {
js = new TextDecoder().decode(js); js = new TextDecoder().decode(js);
} }

View file

@ -1,5 +1,10 @@
import { rewriteJs } from "./js"; import { rewriteJs } from "./js";
export type URLMeta = {
origin: URL;
base: URL;
};
function tryCanParseURL(url: string, origin?: string | URL): URL | null { function tryCanParseURL(url: string, origin?: string | URL): URL | null {
try { try {
return new URL(url, origin); return new URL(url, origin);
@ -9,36 +14,34 @@ function tryCanParseURL(url: string, origin?: string | URL): URL | null {
} }
// something is broken with this but i didn't debug it // something is broken with this but i didn't debug it
export function encodeUrl(url: string | URL, origin?: URL) { export function encodeUrl(url: string | URL, meta: URLMeta) {
if (url instanceof URL) { if (url instanceof URL) {
url = url.href; url = url.href;
} }
if (!origin) { // if (!origin) {
if (location.pathname.startsWith(self.$scramjet.config.prefix + "worker")) { // if (location.pathname.startsWith(self.$scramjet.config.prefix + "worker")) {
origin = new URL(new URL(location.href).searchParams.get("origin")); // origin = new URL(new URL(location.href).searchParams.get("origin"));
} else // } else
origin = new URL( // origin = new URL(
self.$scramjet.codec.decode( // self.$scramjet.codec.decode(
location.href.slice( // location.href.slice(
(location.origin + self.$scramjet.config.prefix).length // (location.origin + self.$scramjet.config.prefix).length
) // )
) // )
); // );
} // }
// is this the correct behavior?
if (!url) url = origin.href;
if (url.startsWith("javascript:")) { if (url.startsWith("javascript:")) {
return "javascript:" + rewriteJs(url.slice("javascript:".length), origin); return "javascript:" + rewriteJs(url.slice("javascript:".length), meta);
} else if (/^(#|mailto|about|data|blob)/.test(url)) { } else if (/^(#|mailto|about|data|blob)/.test(url)) {
// TODO this regex is jank but i'm not fixing it
return url; return url;
} else if (tryCanParseURL(url, origin)) { } else {
return ( return (
location.origin + location.origin +
self.$scramjet.config.prefix + self.$scramjet.config.prefix +
self.$scramjet.codec.encode(new URL(url, origin).href) self.$scramjet.codec.encode(new URL(url, meta.base).href)
); );
} }
} }

View file

@ -1,13 +1,12 @@
import { rewriteJs } from "./js"; import { rewriteJs } from "./js";
import { URLMeta } from "./url";
const clientscripts = ["wasm", "shared", "client"]; const clientscripts = ["wasm", "shared", "client"];
export function rewriteWorkers( export function rewriteWorkers(
js: string | ArrayBuffer, js: string | ArrayBuffer,
type: string, type: string,
origin?: URL meta: URLMeta
) { ) {
origin.search = "";
let str = ""; let str = "";
str += `self.$scramjet = {}; self.$scramjet.config = ${JSON.stringify(self.$scramjet.config)}; str += `self.$scramjet = {}; self.$scramjet.config = ${JSON.stringify(self.$scramjet.config)};
@ -29,7 +28,7 @@ self.$scramjet.codec = self.$scramjet.codecs[self.$scramjet.config.codec];
} }
} }
let rewritten = rewriteJs(js, origin); let rewritten = rewriteJs(js, meta);
if (rewritten instanceof Uint8Array) { if (rewritten instanceof Uint8Array) {
rewritten = new TextDecoder().decode(rewritten); rewritten = new TextDecoder().decode(rewritten);
} }

View file

@ -16,6 +16,15 @@ import {
rewriteWorkers, rewriteWorkers,
} from "../shared"; } from "../shared";
import type { URLMeta } from "../shared/rewriters/url";
function newmeta(url: URL): URLMeta {
return {
origin: url,
base: url,
};
}
export async function swfetch( export async function swfetch(
this: ScramjetServiceWorker, this: ScramjetServiceWorker,
request: Request, request: Request,
@ -25,7 +34,7 @@ export async function swfetch(
if (urlParam.has("url")) { if (urlParam.has("url")) {
return Response.redirect( return Response.redirect(
encodeUrl(urlParam.get("url"), new URL(urlParam.get("url"))) encodeUrl(urlParam.get("url"), newmeta(new URL(urlParam.get("url"))))
); );
} }
@ -124,7 +133,7 @@ async function handleResponse(
client: Client client: Client
): Promise<Response> { ): Promise<Response> {
let responseBody: string | ArrayBuffer | ReadableStream; let responseBody: string | ArrayBuffer | ReadableStream;
const responseHeaders = rewriteHeaders(response.rawHeaders, url); const responseHeaders = rewriteHeaders(response.rawHeaders, newmeta(url));
let maybeHeaders = responseHeaders["set-cookie"] || []; let maybeHeaders = responseHeaders["set-cookie"] || [];
for (const cookie in maybeHeaders) { for (const cookie in maybeHeaders) {
@ -155,7 +164,7 @@ async function handleResponse(
responseBody = rewriteHtml( responseBody = rewriteHtml(
await response.text(), await response.text(),
cookieStore, cookieStore,
url, newmeta(url),
true true
); );
} else { } else {
@ -163,19 +172,19 @@ async function handleResponse(
} }
break; break;
case "script": case "script":
responseBody = rewriteJs(await response.arrayBuffer(), url); responseBody = rewriteJs(await response.arrayBuffer(), newmeta(url));
// Disable threading for now, it's causing issues. // Disable threading for now, it's causing issues.
// responseBody = await this.threadpool.rewriteJs(await responseBody.arrayBuffer(), url.toString()); // responseBody = await this.threadpool.rewriteJs(await responseBody.arrayBuffer(), url.toString());
break; break;
case "style": case "style":
responseBody = rewriteCss(await response.text(), url); responseBody = rewriteCss(await response.text(), newmeta(url));
break; break;
case "sharedworker": case "sharedworker":
case "worker": case "worker":
responseBody = rewriteWorkers( responseBody = rewriteWorkers(
await response.arrayBuffer(), await response.arrayBuffer(),
workertype, workertype,
url newmeta(url)
); );
break; break;
default: default:

View file

@ -145,7 +145,10 @@ export class ScramjetServiceWorker {
const data = await promise.promise; const data = await promise.promise;
delete this.dataworkerpromises[id]; delete this.dataworkerpromises[id];
const rewritten = rewriteWorkers(data, type, new URL(origin)); const rewritten = rewriteWorkers(data, type, {
origin: new URL(origin),
base: new URL(origin),
});
return new Response(rewritten, { return new Response(rewritten, {
headers: { headers: {

View file

@ -25,25 +25,6 @@ scramjet.init("./sw.js");
// } // }
// }); // });
navigator.serviceWorker.onmessage = ({ data }) => {
if (data.scramjet$type === "getLocalStorage") {
const pairs = Object.entries(localStorage);
navigator.serviceWorker.controller.postMessage({
scramjet$type: "getLocalStorage",
scramjet$token: data.scramjet$token,
data: pairs,
});
} else if (data.scramjet$type === "setLocalStorage") {
for (const [key, value] of data.data) {
localStorage.setItem(key, value);
}
navigator.serviceWorker.controller.postMessage({
scramjet$type: "setLocalStorage",
scramjet$token: data.scramjet$token,
});
}
};
const connection = new BareMux.BareMuxConnection("/baremux/worker.js"); const connection = new BareMux.BareMuxConnection("/baremux/worker.js");
const flex = css` const flex = css`
display: flex; display: flex;
@ -150,12 +131,23 @@ function App() {
background-color: #313131; background-color: #313131;
} }
`; `;
this.url = store.url;
const frame = scramjet.createFrame(); const frame = scramjet.createFrame();
frame.addEventListener("urlchange", (e) => { frame.addEventListener("urlchange", (e) => {
if (!e.url) return;
this.url = e.url; this.url = e.url;
}); });
frame.frame.addEventListener("load", () => {
let url = frame.frame.contentWindow.location.href;
if (!url) return;
if (url === "about:blank") return;
this.url = $scramjet.codecs.plain.decode(
url.substring((location.href + "/scramjet").length)
);
});
return html` return html`
<div> <div>
@ -193,7 +185,7 @@ function App() {
e e
) => { ) => {
this.url = e.target.value; this.url = e.target.value;
}} on:keyup=${(e) => e.keyCode == 13 && frame.go(e.target.value) && (store.url = this.url)}></input> }} on:keyup=${(e) => e.keyCode == 13 && (store.url = this.url) && frame.go(e.target.value)}></input>
<button on:click=${() => frame.forward()}>-&gt;</button> <button on:click=${() => frame.forward()}>-&gt;</button>
</div> </div>
${frame.frame} ${frame.frame}