This commit is contained in:
velzie 2024-08-10 15:03:54 -04:00
parent 7b8d5d09b2
commit 3bc8dd34b9
No known key found for this signature in database
GPG key ID: 048413F95F0DDE1F
14 changed files with 144 additions and 93 deletions

View file

@ -1,5 +1,5 @@
import { createLocationProxy } from "./location"; import { createLocationProxy } from "./location";
import { decodeUrl } from "./shared"; import { CookieStore, decodeUrl } from "./shared";
import { createDocumentProxy, createWindowProxy } from "./window"; import { createDocumentProxy, createWindowProxy } from "./window";
declare global { declare global {
@ -46,6 +46,8 @@ export class ScramjetClient {
windowProxy: any; windowProxy: any;
locationProxy: any; locationProxy: any;
cookieStore = new CookieStore();
eventcallbacks: Map< eventcallbacks: Map<
any, any,
[ [
@ -68,13 +70,17 @@ export class ScramjetClient {
global[ScramjetClient.SCRAMJET] = this; global[ScramjetClient.SCRAMJET] = this;
} }
loadcookies(cookiestr: string) {
this.cookieStore.load(cookiestr);
}
hook() { hook() {
// @ts-ignore // @ts-ignore
const context = import.meta.webpackContext(".", { const context = import.meta.webpackContext(".", {
recursive: true, recursive: true,
}); });
let modules = []; const modules = [];
for (const key of context.keys()) { for (const key of context.keys()) {
const module = context(key); const module = context(key);

View file

@ -1,37 +1,12 @@
import { parse } from "set-cookie-parser";
import { ScramjetClient } from "../client"; import { ScramjetClient } from "../client";
import IDBMapSync from "@webreflection/idb-map/sync";
export default function (client: ScramjetClient, self: typeof window) { export default function (client: ScramjetClient, self: typeof window) {
let cookieStore = new IDBMapSync(client.url.host, {
durability: "relaxed",
prefix: "Cookies",
});
client.Trap("Document.prototype.cookie", { client.Trap("Document.prototype.cookie", {
get(ctx) { get() {
let cookies = cookieStore.entries(); return client.cookieStore.getCookies(client.url, true);
if (client.url.protocol !== "https:") {
cookies = cookies.filter(([_k, v]) => !v.args.includes(["Secure"]));
}
cookies = cookies.filter(([_k, v]) => !v.args.includes(["HttpOnly"]));
cookies = Array.from(cookies.map(([k, v]) => `${k}=${v.value}`));
return cookies.join("; ");
}, },
set(ctx, value: string) { set(ctx, value: string) {
// dbg.debug("setting cookie", value); client.cookieStore.setCookies([value], client.url);
const cookie = parse(value)[0];
let date = new Date();
let expires = cookie.expires;
// dbg.error("expires", expires);
// if (expires instanceof Date) {
// if (isNaN(expires.getTime())) return;
// if (expires.getTime() < date.getTime()) return;
// }
// set.call(document, `${cookie.name}=${cookie.value}`);
}, },
}); });

View file

@ -65,7 +65,7 @@ export default function (client: ScramjetClient, self: typeof window) {
) { ) {
value = encodeUrl(value); value = encodeUrl(value);
} else if (attr === "srcdoc") { } else if (attr === "srcdoc") {
value = rewriteHtml(value); value = rewriteHtml(value, client.cookieStore);
} else if (["srcset", "imagesrcset"].includes(attr)) { } else if (["srcset", "imagesrcset"].includes(attr)) {
value = rewriteSrcset(value); value = rewriteSrcset(value);
} }
@ -114,7 +114,7 @@ export default function (client: ScramjetClient, self: typeof window) {
argArray[1] = encodeUrl(argArray[1]); argArray[1] = encodeUrl(argArray[1]);
} else if (argArray[0] === "srcdoc") { } else if (argArray[0] === "srcdoc") {
// TODO: this will rewrite with the wrong url in mind for iframes!! // TODO: this will rewrite with the wrong url in mind for iframes!!
argArray[1] = rewriteHtml(argArray[1]); argArray[1] = rewriteHtml(argArray[1], client.cookieStore);
} else if (["srcset", "imagesrcset"].includes(argArray[0])) { } else if (["srcset", "imagesrcset"].includes(argArray[0])) {
argArray[1] = rewriteSrcset(argArray[1]); argArray[1] = rewriteSrcset(argArray[1]);
} else if (argArray[1] === "style") { } else if (argArray[1] === "style") {
@ -139,7 +139,7 @@ export default function (client: ScramjetClient, self: typeof window) {
} else if (this instanceof self.HTMLStyleElement) { } else if (this instanceof self.HTMLStyleElement) {
value = rewriteCss(value); value = rewriteCss(value);
} else { } else {
value = rewriteHtml(value); value = rewriteHtml(value, client.cookieStore);
} }
return innerHTML.set.call(this, value); return innerHTML.set.call(this, value);

View file

@ -15,6 +15,10 @@ dbg.log("scrammin");
// if it already exists, that means the handlers have probably already been setup by the parent document // if it already exists, that means the handlers have probably already been setup by the parent document
if (!(ScramjetClient.SCRAMJET in self)) { if (!(ScramjetClient.SCRAMJET in self)) {
const client = new ScramjetClient(self); const client = new ScramjetClient(self);
client.loadcookies(self.COOKIE);
delete self.COOKIE;
client.hook(); client.hook();
if (isemulatedsw) { if (isemulatedsw) {

View file

@ -9,6 +9,7 @@ export const {
rewriteHeaders, rewriteHeaders,
rewriteWorkers, rewriteWorkers,
}, },
CookieStore,
} = self.$scramjet.shared; } = self.$scramjet.shared;
export const config = self.$scramjet.config; export const config = self.$scramjet.config;

76
src/shared/cookie.ts Normal file
View file

@ -0,0 +1,76 @@
import parse from "set-cookie-parser";
export type Cookie = {
name: string;
value: string;
path?: string;
expires?: string;
maxAge?: number;
domain?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: "strict" | "lax" | "none";
};
export class CookieStore {
private cookies: Record<string, Cookie> = {};
async setCookies(cookies: string[], url: URL) {
for (const str of cookies) {
const parsed = parse(str);
const domain = parsed.domain;
const sameSite = parsed.sameSite;
const cookie: Cookie = {
domain,
sameSite,
...parsed[0],
};
dbg.log("cookie", cookie);
if (!cookie.domain) cookie.domain = "." + url.hostname;
if (!cookie.domain.startsWith(".")) cookie.domain = "." + cookie.domain;
if (!cookie.path) cookie.path = "/";
if (!cookie.sameSite) cookie.sameSite = "lax";
if (cookie.expires) cookie.expires = cookie.expires.toString();
const id = `${cookie.domain}@${cookie.path}@${cookie.name}`;
this.cookies[id] = cookie;
}
}
getCookies(url: URL, fromJs: boolean): string {
const now = new Date();
const cookies = Object.values(this.cookies);
const validCookies: Cookie[] = [];
for (const cookie of cookies) {
if (cookie.expires && new Date(cookie.expires) < now) {
delete this.cookies[`${cookie.domain}@${cookie.path}@${cookie.name}`];
continue;
}
if (cookie.secure && url.protocol !== "https:") continue;
if (cookie.httpOnly && fromJs) continue;
if (!url.pathname.startsWith(cookie.path)) continue;
if (cookie.domain.startsWith(".")) {
if (!url.hostname.endsWith(cookie.domain.slice(1))) continue;
}
validCookies.push(cookie);
}
return validCookies
.map((cookie) => `${cookie.name}=${cookie.value}`)
.join("; ");
}
load(cookies: string) {
this.cookies = JSON.parse(cookies);
}
dump(): string {
return JSON.stringify(this.cookies);
}
}

View file

@ -8,6 +8,7 @@ import { isScramjetFile } from "./rewriters/html";
import { BareClient } from "@mercuryworkshop/bare-mux"; import { BareClient } from "@mercuryworkshop/bare-mux";
import { parseDomain } from "parse-domain"; import { parseDomain } from "parse-domain";
import { ScramjetHeaders } from "./headers"; import { ScramjetHeaders } from "./headers";
import { CookieStore } from "./cookie";
self.$scramjet.shared = { self.$scramjet.shared = {
util: { util: {
@ -28,6 +29,7 @@ self.$scramjet.shared = {
rewriteHeaders, rewriteHeaders,
rewriteWorkers, rewriteWorkers,
}, },
CookieStore,
}; };
if ("document" in self && document.currentScript) { if ("document" in self && document.currentScript) {

View file

@ -43,11 +43,11 @@ export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) {
); );
}); });
if (headers["link"]) { // if (headers["link"]) {
headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) => // headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) =>
encodeUrl(match, origin) // encodeUrl(match, origin)
); // );
} // }
return headers; return headers;
} }

View file

@ -5,6 +5,7 @@ import render from "dom-serializer";
import { encodeUrl } from "./url"; import { encodeUrl } from "./url";
import { rewriteCss } from "./css"; import { rewriteCss } from "./css";
import { rewriteJs } from "./js"; import { rewriteJs } from "./js";
import { CookieStore } from "../cookie";
export function isScramjetFile(src: string) { export function isScramjetFile(src: string) {
let bool = false; let bool = false;
@ -15,19 +16,23 @@ export function isScramjetFile(src: string) {
return bool; return bool;
} }
export function rewriteHtml(html: string, origin?: URL) { export function rewriteHtml(
html: string,
cookieStore: CookieStore,
origin?: URL
) {
const handler = new DomHandler((err, dom) => dom); const handler = new DomHandler((err, dom) => dom);
const parser = new Parser(handler); const parser = new Parser(handler);
parser.write(html); parser.write(html);
parser.end(); parser.end();
return render(traverseParsedHtml(handler.root, origin)); return render(traverseParsedHtml(handler.root, cookieStore, origin));
} }
// i need to add the attributes in during rewriting // i need to add the attributes in during rewriting
function traverseParsedHtml(node, origin?: URL) { function traverseParsedHtml(node, cookieStore: CookieStore, origin?: URL) {
/* csp attributes */ /* csp attributes */
for (const cspAttr of ["nonce", "integrity", "csp"]) { for (const cspAttr of ["nonce", "integrity", "csp"]) {
if (hasAttrib(node, cspAttr)) { if (hasAttrib(node, cspAttr)) {
@ -86,7 +91,7 @@ function traverseParsedHtml(node, origin?: URL) {
} }
if (hasAttrib(node, "srcdoc")) if (hasAttrib(node, "srcdoc"))
node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin); node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, cookieStore, origin);
if (hasAttrib(node, "style")) if (hasAttrib(node, "style"))
node.attribs.style = rewriteCss(node.attribs.style, origin); node.attribs.style = rewriteCss(node.attribs.style, origin);
@ -122,6 +127,8 @@ function traverseParsedHtml(node, origin?: URL) {
if (node.name === "head") { if (node.name === "head") {
const scripts = []; const scripts = [];
const dump = JSON.stringify(cookieStore.dump());
scripts.push( scripts.push(
new Element("script", { new Element("script", {
src: self.$scramjet.config["wasm"], src: self.$scramjet.config["wasm"],
@ -137,6 +144,7 @@ function traverseParsedHtml(node, origin?: URL) {
"data:application/javascript;base64," + "data:application/javascript;base64," +
btoa( btoa(
` `
self.COOKIE = ${dump};
self.$scramjet.config = ${JSON.stringify(self.$scramjet.config)}; self.$scramjet.config = ${JSON.stringify(self.$scramjet.config)};
self.$scramjet.codec = self.$scramjet.codecs[self.$scramjet.config.codec]; self.$scramjet.codec = self.$scramjet.codecs[self.$scramjet.config.codec];
if ("document" in self && document.currentScript) { if ("document" in self && document.currentScript) {
@ -164,6 +172,7 @@ function traverseParsedHtml(node, origin?: URL) {
for (const childNode in node.childNodes) { for (const childNode in node.childNodes) {
node.childNodes[childNode] = traverseParsedHtml( node.childNodes[childNode] = traverseParsedHtml(
node.childNodes[childNode], node.childNodes[childNode],
cookieStore,
origin origin
); );
} }

View file

@ -36,7 +36,7 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) {
} }
const after = performance.now(); const after = performance.now();
dbg.debug("Rewrite took", Math.floor((after - before) * 10) / 10, "ms"); // dbg.debug("Rewrite took", Math.floor((after - before) * 10) / 10, "ms");
return js; return js;
} }

3
src/types.d.ts vendored
View file

@ -10,6 +10,7 @@ import type { Codec } from "./codecs";
import { BareClient } from "@mercuryworkshop/bare-mux"; import { BareClient } from "@mercuryworkshop/bare-mux";
import { parseDomain } from "parse-domain"; import { parseDomain } from "parse-domain";
import { ScramjetHeaders } from "./shared/headers"; import { ScramjetHeaders } from "./shared/headers";
import { CookieStore } from "./shared/cookie";
interface ScramjetConfig { interface ScramjetConfig {
prefix: string; prefix: string;
@ -49,6 +50,7 @@ declare global {
isScramjetFile: typeof isScramjetFile; isScramjetFile: typeof isScramjetFile;
parseDomain: typeof parseDomain; parseDomain: typeof parseDomain;
}; };
CookieStore: typeof CookieStore;
}; };
config: ScramjetConfig; config: ScramjetConfig;
codecs: { codecs: {
@ -59,6 +61,7 @@ declare global {
}; };
codec: Codec; codec: Codec;
}; };
COOKIE: string;
WASM: string; WASM: string;
ScramjetController: typeof ScramjetController; ScramjetController: typeof ScramjetController;
} }

View file

@ -1,21 +0,0 @@
export type Cookie = {
name: string;
value: string;
path?: string;
expires?: Date;
maxAge?: number;
domain?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: "strict" | "lax" | "none";
};
class CookieStore {
private cookies: Cookie[] = [];
async load() {
return this.cookies;
}
}
export const cookieStore = new CookieStore();

View file

@ -4,8 +4,7 @@ import { ParseResultType } from "parse-domain";
import { ScramjetServiceWorker } from "."; import { ScramjetServiceWorker } from ".";
import { renderError } from "./error"; import { renderError } from "./error";
import { FakeServiceWorker } from "./fakesw"; import { FakeServiceWorker } from "./fakesw";
import parse from "set-cookie-parser"; import { CookieStore } from "../shared/cookie";
import { cookieStore } from "./cookie";
const { encodeUrl, decodeUrl } = self.$scramjet.shared.url; const { encodeUrl, decodeUrl } = self.$scramjet.shared.url;
const { rewriteHeaders, rewriteHtml, rewriteJs, rewriteCss, rewriteWorkers } = const { rewriteHeaders, rewriteHtml, rewriteJs, rewriteCss, rewriteWorkers } =
@ -75,23 +74,10 @@ export async function swfetch(
headers.set("Referer", decodeUrl(request.referrer)); headers.set("Referer", decodeUrl(request.referrer));
let cookies = [...(await cookieStore.entries())]; const cookies = this.cookieStore.getCookies(url, false);
console.log("cookies", cookies);
// if (url.protocol !== "https:") {
// cookies = cookies.filter(([_k, v]) => !v.args.includes(["Secure"]));
// }
cookies = cookies.filter(
([_k, v]) =>
v.args.domain.includes(url.hostname) ||
url.hostname.includes(v.args.domain)
);
cookies = cookies.filter(([_k, v]) => v.value !== "");
cookies = Array.from(cookies.map(([k, v]) => `${k}=${v.value}`));
if (cookies.length) { if (cookies.length) {
headers.set("Cookie", cookies.join("; ")); headers.set("Cookie", cookies);
} }
// TODO this is wrong somehow // TODO this is wrong somehow
@ -112,7 +98,12 @@ export async function swfetch(
duplex: "half", duplex: "half",
}); });
return await handleResponse(url, request.destination, response); return await handleResponse(
url,
request.destination,
response,
this.cookieStore
);
} catch (err) { } catch (err) {
console.error("ERROR FROM SERVICE WORKER FETCH", err); console.error("ERROR FROM SERVICE WORKER FETCH", err);
if (!["document", "iframe"].includes(request.destination)) if (!["document", "iframe"].includes(request.destination))
@ -125,12 +116,17 @@ export async function swfetch(
async function handleResponse( async function handleResponse(
url: URL, url: URL,
destination: RequestDestination, destination: RequestDestination,
response: BareResponseFetch response: BareResponseFetch,
cookieStore: CookieStore
): 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, url);
await handleCookies(url, (responseHeaders["set-cookie"] || []) as string[]); await handleCookies(
url,
cookieStore,
(responseHeaders["set-cookie"] || []) as string[]
);
for (const header in responseHeaders) { for (const header in responseHeaders) {
// flatten everything past here // flatten everything past here
@ -143,7 +139,7 @@ async function handleResponse(
case "iframe": case "iframe":
case "document": case "document":
if (responseHeaders["content-type"]?.startsWith("text/html")) { if (responseHeaders["content-type"]?.startsWith("text/html")) {
responseBody = rewriteHtml(await response.text(), url); responseBody = rewriteHtml(await response.text(), cookieStore, url);
} else { } else {
responseBody = response.body; responseBody = response.body;
} }
@ -202,14 +198,12 @@ async function handleResponse(
}); });
} }
async function handleCookies(url: URL, maybeHeaders: string[] | string) { async function handleCookies(
const cookies = await cookieStore.load(); url: URL,
cookieStore: CookieStore,
maybeHeaders: string[] | string
) {
const headers = maybeHeaders instanceof Array ? maybeHeaders : [maybeHeaders]; const headers = maybeHeaders instanceof Array ? maybeHeaders : [maybeHeaders];
for (const cookie of headers) { await cookieStore.setCookies(headers, url);
const parsed = parse(cookie)[0];
console.error("set-cookie", parsed);
cookies.push(parsed);
}
} }

View file

@ -17,6 +17,8 @@ export class ScramjetServiceWorker {
syncPool: Record<number, (val?: any) => void> = {}; syncPool: Record<number, (val?: any) => void> = {};
synctoken = 0; synctoken = 0;
cookieStore = new self.$scramjet.shared.CookieStore();
serviceWorkers: FakeServiceWorker[] = []; serviceWorkers: FakeServiceWorker[] = [];
constructor() { constructor() {