diff --git a/src/client/client.ts b/src/client/client.ts index 316d180..5ad5328 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,5 +1,5 @@ import { createLocationProxy } from "./location"; -import { decodeUrl } from "./shared"; +import { CookieStore, decodeUrl } from "./shared"; import { createDocumentProxy, createWindowProxy } from "./window"; declare global { @@ -46,6 +46,8 @@ export class ScramjetClient { windowProxy: any; locationProxy: any; + cookieStore = new CookieStore(); + eventcallbacks: Map< any, [ @@ -68,13 +70,17 @@ export class ScramjetClient { global[ScramjetClient.SCRAMJET] = this; } + loadcookies(cookiestr: string) { + this.cookieStore.load(cookiestr); + } + hook() { // @ts-ignore const context = import.meta.webpackContext(".", { recursive: true, }); - let modules = []; + const modules = []; for (const key of context.keys()) { const module = context(key); diff --git a/src/client/dom/cookie.ts b/src/client/dom/cookie.ts index 46ce41a..ccffd7a 100644 --- a/src/client/dom/cookie.ts +++ b/src/client/dom/cookie.ts @@ -1,37 +1,12 @@ -import { parse } from "set-cookie-parser"; import { ScramjetClient } from "../client"; -import IDBMapSync from "@webreflection/idb-map/sync"; export default function (client: ScramjetClient, self: typeof window) { - let cookieStore = new IDBMapSync(client.url.host, { - durability: "relaxed", - prefix: "Cookies", - }); - client.Trap("Document.prototype.cookie", { - get(ctx) { - let cookies = cookieStore.entries(); - 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("; "); + get() { + return client.cookieStore.getCookies(client.url, true); }, set(ctx, value: string) { - // dbg.debug("setting cookie", value); - 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}`); + client.cookieStore.setCookies([value], client.url); }, }); diff --git a/src/client/dom/element.ts b/src/client/dom/element.ts index 7d6e207..eb53571 100644 --- a/src/client/dom/element.ts +++ b/src/client/dom/element.ts @@ -65,7 +65,7 @@ export default function (client: ScramjetClient, self: typeof window) { ) { value = encodeUrl(value); } else if (attr === "srcdoc") { - value = rewriteHtml(value); + value = rewriteHtml(value, client.cookieStore); } else if (["srcset", "imagesrcset"].includes(attr)) { value = rewriteSrcset(value); } @@ -114,7 +114,7 @@ export default function (client: ScramjetClient, self: typeof window) { argArray[1] = encodeUrl(argArray[1]); } else if (argArray[0] === "srcdoc") { // 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])) { argArray[1] = rewriteSrcset(argArray[1]); } else if (argArray[1] === "style") { @@ -139,7 +139,7 @@ export default function (client: ScramjetClient, self: typeof window) { } else if (this instanceof self.HTMLStyleElement) { value = rewriteCss(value); } else { - value = rewriteHtml(value); + value = rewriteHtml(value, client.cookieStore); } return innerHTML.set.call(this, value); diff --git a/src/client/index.ts b/src/client/index.ts index 4138753..4d3c30c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -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 (!(ScramjetClient.SCRAMJET in self)) { const client = new ScramjetClient(self); + + client.loadcookies(self.COOKIE); + delete self.COOKIE; + client.hook(); if (isemulatedsw) { diff --git a/src/client/shared.ts b/src/client/shared.ts index 1115c30..df84d99 100644 --- a/src/client/shared.ts +++ b/src/client/shared.ts @@ -9,6 +9,7 @@ export const { rewriteHeaders, rewriteWorkers, }, + CookieStore, } = self.$scramjet.shared; export const config = self.$scramjet.config; diff --git a/src/shared/cookie.ts b/src/shared/cookie.ts new file mode 100644 index 0000000..10324eb --- /dev/null +++ b/src/shared/cookie.ts @@ -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 = {}; + + 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); + } +} diff --git a/src/shared/index.ts b/src/shared/index.ts index 11f18f8..82ea53a 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -8,6 +8,7 @@ import { isScramjetFile } from "./rewriters/html"; import { BareClient } from "@mercuryworkshop/bare-mux"; import { parseDomain } from "parse-domain"; import { ScramjetHeaders } from "./headers"; +import { CookieStore } from "./cookie"; self.$scramjet.shared = { util: { @@ -28,6 +29,7 @@ self.$scramjet.shared = { rewriteHeaders, rewriteWorkers, }, + CookieStore, }; if ("document" in self && document.currentScript) { diff --git a/src/shared/rewriters/headers.ts b/src/shared/rewriters/headers.ts index f00088a..30bd1de 100644 --- a/src/shared/rewriters/headers.ts +++ b/src/shared/rewriters/headers.ts @@ -43,11 +43,11 @@ export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) { ); }); - if (headers["link"]) { - headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) => - encodeUrl(match, origin) - ); - } + // if (headers["link"]) { + // headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) => + // encodeUrl(match, origin) + // ); + // } return headers; } diff --git a/src/shared/rewriters/html.ts b/src/shared/rewriters/html.ts index 527b2f2..b2c0c6c 100644 --- a/src/shared/rewriters/html.ts +++ b/src/shared/rewriters/html.ts @@ -5,6 +5,7 @@ import render from "dom-serializer"; import { encodeUrl } from "./url"; import { rewriteCss } from "./css"; import { rewriteJs } from "./js"; +import { CookieStore } from "../cookie"; export function isScramjetFile(src: string) { let bool = false; @@ -15,19 +16,23 @@ export function isScramjetFile(src: string) { 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 parser = new Parser(handler); parser.write(html); parser.end(); - return render(traverseParsedHtml(handler.root, origin)); + return render(traverseParsedHtml(handler.root, cookieStore, origin)); } // i need to add the attributes in during rewriting -function traverseParsedHtml(node, origin?: URL) { +function traverseParsedHtml(node, cookieStore: CookieStore, origin?: URL) { /* csp attributes */ for (const cspAttr of ["nonce", "integrity", "csp"]) { if (hasAttrib(node, cspAttr)) { @@ -86,7 +91,7 @@ function traverseParsedHtml(node, origin?: URL) { } 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")) node.attribs.style = rewriteCss(node.attribs.style, origin); @@ -122,6 +127,8 @@ function traverseParsedHtml(node, origin?: URL) { if (node.name === "head") { const scripts = []; + const dump = JSON.stringify(cookieStore.dump()); + scripts.push( new Element("script", { src: self.$scramjet.config["wasm"], @@ -137,6 +144,7 @@ function traverseParsedHtml(node, origin?: URL) { "data:application/javascript;base64," + btoa( ` + self.COOKIE = ${dump}; self.$scramjet.config = ${JSON.stringify(self.$scramjet.config)}; self.$scramjet.codec = self.$scramjet.codecs[self.$scramjet.config.codec]; if ("document" in self && document.currentScript) { @@ -164,6 +172,7 @@ function traverseParsedHtml(node, origin?: URL) { for (const childNode in node.childNodes) { node.childNodes[childNode] = traverseParsedHtml( node.childNodes[childNode], + cookieStore, origin ); } diff --git a/src/shared/rewriters/js.ts b/src/shared/rewriters/js.ts index c3d1a39..ab4bd05 100644 --- a/src/shared/rewriters/js.ts +++ b/src/shared/rewriters/js.ts @@ -36,7 +36,7 @@ export function rewriteJs(js: string | ArrayBuffer, origin?: URL) { } 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; } diff --git a/src/types.d.ts b/src/types.d.ts index 8554aaf..a816505 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -10,6 +10,7 @@ import type { Codec } from "./codecs"; import { BareClient } from "@mercuryworkshop/bare-mux"; import { parseDomain } from "parse-domain"; import { ScramjetHeaders } from "./shared/headers"; +import { CookieStore } from "./shared/cookie"; interface ScramjetConfig { prefix: string; @@ -49,6 +50,7 @@ declare global { isScramjetFile: typeof isScramjetFile; parseDomain: typeof parseDomain; }; + CookieStore: typeof CookieStore; }; config: ScramjetConfig; codecs: { @@ -59,6 +61,7 @@ declare global { }; codec: Codec; }; + COOKIE: string; WASM: string; ScramjetController: typeof ScramjetController; } diff --git a/src/worker/cookie.ts b/src/worker/cookie.ts deleted file mode 100644 index 185a8c3..0000000 --- a/src/worker/cookie.ts +++ /dev/null @@ -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(); diff --git a/src/worker/fetch.ts b/src/worker/fetch.ts index c2002eb..fe573f2 100644 --- a/src/worker/fetch.ts +++ b/src/worker/fetch.ts @@ -4,8 +4,7 @@ import { ParseResultType } from "parse-domain"; import { ScramjetServiceWorker } from "."; import { renderError } from "./error"; import { FakeServiceWorker } from "./fakesw"; -import parse from "set-cookie-parser"; -import { cookieStore } from "./cookie"; +import { CookieStore } from "../shared/cookie"; const { encodeUrl, decodeUrl } = self.$scramjet.shared.url; const { rewriteHeaders, rewriteHtml, rewriteJs, rewriteCss, rewriteWorkers } = @@ -75,23 +74,10 @@ export async function swfetch( headers.set("Referer", decodeUrl(request.referrer)); - let cookies = [...(await cookieStore.entries())]; - 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}`)); + const cookies = this.cookieStore.getCookies(url, false); if (cookies.length) { - headers.set("Cookie", cookies.join("; ")); + headers.set("Cookie", cookies); } // TODO this is wrong somehow @@ -112,7 +98,12 @@ export async function swfetch( duplex: "half", }); - return await handleResponse(url, request.destination, response); + return await handleResponse( + url, + request.destination, + response, + this.cookieStore + ); } catch (err) { console.error("ERROR FROM SERVICE WORKER FETCH", err); if (!["document", "iframe"].includes(request.destination)) @@ -125,12 +116,17 @@ export async function swfetch( async function handleResponse( url: URL, destination: RequestDestination, - response: BareResponseFetch + response: BareResponseFetch, + cookieStore: CookieStore ): Promise { let responseBody: string | ArrayBuffer | ReadableStream; 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) { // flatten everything past here @@ -143,7 +139,7 @@ async function handleResponse( case "iframe": case "document": if (responseHeaders["content-type"]?.startsWith("text/html")) { - responseBody = rewriteHtml(await response.text(), url); + responseBody = rewriteHtml(await response.text(), cookieStore, url); } else { responseBody = response.body; } @@ -202,14 +198,12 @@ async function handleResponse( }); } -async function handleCookies(url: URL, maybeHeaders: string[] | string) { - const cookies = await cookieStore.load(); +async function handleCookies( + url: URL, + cookieStore: CookieStore, + maybeHeaders: string[] | string +) { const headers = maybeHeaders instanceof Array ? maybeHeaders : [maybeHeaders]; - for (const cookie of headers) { - const parsed = parse(cookie)[0]; - console.error("set-cookie", parsed); - - cookies.push(parsed); - } + await cookieStore.setCookies(headers, url); } diff --git a/src/worker/index.ts b/src/worker/index.ts index 2c19551..6bbfe81 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -17,6 +17,8 @@ export class ScramjetServiceWorker { syncPool: Record void> = {}; synctoken = 0; + cookieStore = new self.$scramjet.shared.CookieStore(); + serviceWorkers: FakeServiceWorker[] = []; constructor() {