diff --git a/README.md b/README.md index fa5e320..e2c7f37 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,5 @@ Running `pnpm dev` will build Scramjet and start a dev server on localhost:1337. - Only thing rewritten currently are imports and exports - Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present - Write client APIs +- The DAMN SCRIPTS not being executed +- esbuild bundling more than what it's supposed to (instead of using imports, it bundles the whole file which is not what we want) \ No newline at end of file diff --git a/esbuild.dev.js b/esbuild.dev.js index 0a338d5..9c5fee4 100644 --- a/esbuild.dev.js +++ b/esbuild.dev.js @@ -15,7 +15,7 @@ const bare = createBareServer("/bare/", { }); const fastify = Fastify({ - serverFactory: (handler, opts) => { + serverFactory: (handler) => { return createServer() .on("request", (req, res) => { if (bare.shouldRoute(req)) { @@ -53,12 +53,14 @@ const devServer = await context({ worker: "./src/worker/index.ts", codecs: "./src/codecs/index.ts", config: "./src/scramjet.config.ts", + html: "./src/html/index.ts" }, entryNames: "scramjet.[name]", outdir: "./dist", bundle: true, sourcemap: true, logLevel: "info", + format: "esm", plugins: [ copy({ resolveFrom: "cwd", diff --git a/esbuild.js b/esbuild.js index a84d92b..9f01e71 100644 --- a/esbuild.js +++ b/esbuild.js @@ -9,6 +9,7 @@ const scramjetBuild = await build({ worker: "./src/worker/index.ts", codecs: "./src/codecs/index.ts", config: "./src/scramjet.config.ts", + html: "./scramjet.html.ts" }, entryNames: "scramjet.[name]", outdir: "./dist", @@ -19,7 +20,8 @@ const scramjetBuild = await build({ logLevel: "info", metafile: true, treeShaking: true, - minify: true + minify: true, + format: "esm" }); writeFileSync("./meta.json", JSON.stringify(scramjetBuild.metafile)); diff --git a/src/bundle/index.ts b/src/bundle/index.ts index 7be93f5..f780ba9 100644 --- a/src/bundle/index.ts +++ b/src/bundle/index.ts @@ -1,6 +1,6 @@ import { encodeUrl, decodeUrl } from "./rewriters/url"; import { rewriteCss } from "./rewriters/css"; -import { rewriteHtml, rewriteSrcset } from "./rewriters/html"; +import { rewriteSrcset } from "./rewriters/srcset"; import { rewriteJs } from "./rewriters/js"; import { rewriteHeaders } from "./rewriters/headers"; @@ -13,24 +13,4 @@ export function isScramjetFile(src: string) { return bool; } -const bundle = { - rewriters: { - url: { - encodeUrl, decodeUrl - }, - rewriteCss, - rewriteHtml, - rewriteSrcset, - rewriteJs, - rewriteHeaders - }, - isScramjetFile -} - -declare global { - interface Window { - __scramjet$bundle: typeof bundle; - } -} - -self.__scramjet$bundle = bundle; \ No newline at end of file +export { encodeUrl, decodeUrl, rewriteCss, rewriteSrcset, rewriteJs, rewriteHeaders }; \ No newline at end of file diff --git a/src/bundle/rewriters/html.ts b/src/bundle/rewriters/html.ts deleted file mode 100644 index e0deb3b..0000000 --- a/src/bundle/rewriters/html.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Parser } from "htmlparser2"; -import { DomHandler, Element } from "domhandler"; -import { hasAttrib } from "domutils"; -import render from "dom-serializer"; -import { encodeUrl } from "./url"; -import { rewriteCss } from "./css"; -import { rewriteJs } from "./js"; -import { isScramjetFile } from "../"; - -export function rewriteHtml(html: string, 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)); -} - -// i need to add the attributes in during rewriting - -function traverseParsedHtml(node, origin?: URL) { - /* csp attributes */ - if (hasAttrib(node, "nonce")) delete node.attribs.nonce; - if (hasAttrib(node, "integrity")) delete node.attribs.integrity; - if (hasAttrib(node, "csp")) delete node.attribs.csp; - - /* url attributes */ - if (hasAttrib(node, "src") && !isScramjetFile(node.attribs.src)) node.attribs.src = encodeUrl(node.attribs.src, origin); - if (hasAttrib(node, "href")) node.attribs.href = encodeUrl(node.attribs.href, origin); - if (hasAttrib(node, "data")) node.attribs.data = encodeUrl(node.attribs.data, origin); - if (hasAttrib(node, "action")) node.attribs.action = encodeUrl(node.attribs.action, origin); - if (hasAttrib(node, "formaction")) node.attribs.formaction = encodeUrl(node.attribs.formaction, origin); - - /* other */ - if (hasAttrib(node, "srcdoc")) node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin); - if (hasAttrib(node, "srcset")) node.attribs.srcset = rewriteSrcset(node.attribs.srcset, origin); - if (hasAttrib(node, "imagesrcset")) node.attribs.imagesrcset = rewriteSrcset(node.attribs.imagesrcset, origin); - if (hasAttrib(node, "style")) node.attribs.style = rewriteCss(node.attribs.style, origin); - - if (node.name === "style" && node.children[0] !== undefined) node.children[0].data = rewriteCss(node.children[0].data, origin); - if (node.name === "script" && /(application|text)\/javascript|importmap|undefined/.test(node.attribs.type) && node.children[0] !== undefined) node.children[0].data = rewriteJs(node.children[0].data, origin); - if (node.name === "meta" && hasAttrib(node, "http-equiv")) { - if (node.attribs["http-equiv"] === "content-security-policy") { - node = {}; - } else if (node.attribs["http-equiv"] === "refresh" && node.attribs.content.includes("url")) { - const contentArray = node.attribs.content.split("url="); - contentArray[1] = encodeUrl(contentArray[1].trim(), origin); - node.attribs.content = contentArray.join("url="); - } - } - - if (node.name === "head") { - const scramjetScripts = []; - ["codecs", "config", "bundle", "client"].forEach((script) => { - scramjetScripts.push(new Element("script", { - src: self.__scramjet$config[script] - })); - }); - - node.children.unshift(...scramjetScripts); - } - - if (node.childNodes) { - for (const childNode in node.childNodes) { - node.childNodes[childNode] = traverseParsedHtml(node.childNodes[childNode], origin); - } - } - - return node; -} - -// stole from osana lmao -export function rewriteSrcset(srcset: string, origin?: URL) { - const urls = srcset.split(/ [0-9]+x,? ?/g); - if (!urls) return ""; - const sufixes = srcset.match(/ [0-9]+x,? ?/g); - if (!sufixes) return ""; - const rewrittenUrls = urls.map((url, i) => { - if (url && sufixes[i]) { - return encodeUrl(url, origin) + sufixes[i]; - } - }); - - return rewrittenUrls.join(""); -} diff --git a/src/bundle/rewriters/srcset.ts b/src/bundle/rewriters/srcset.ts new file mode 100644 index 0000000..cc3db7b --- /dev/null +++ b/src/bundle/rewriters/srcset.ts @@ -0,0 +1,15 @@ +import { encodeUrl } from "./url"; + +export function rewriteSrcset(srcset: string, origin?: URL) { + const urls = srcset.split(/ [0-9]+x,? ?/g); + if (!urls) return ""; + const sufixes = srcset.match(/ [0-9]+x,? ?/g); + if (!sufixes) return ""; + const rewrittenUrls = urls.map((url, i) => { + if (url && sufixes[i]) { + return encodeUrl(url, origin) + sufixes[i]; + } + }); + + return rewrittenUrls.join(""); +} diff --git a/src/bundle/rewriters/url.ts b/src/bundle/rewriters/url.ts index c7f8d11..0e2d9ea 100644 --- a/src/bundle/rewriters/url.ts +++ b/src/bundle/rewriters/url.ts @@ -1,6 +1,6 @@ import { rewriteJs } from "./js"; -function canParseUrl(url: string, origin?: URL) { +function canParseUrl(url: string, origin: string | URL) { try { new URL(url, origin); @@ -11,14 +11,16 @@ function canParseUrl(url: string, origin?: URL) { } // something is broken with this but i didn't debug it -export function encodeUrl(url: string, origin?: URL) { +export function encodeUrl(url: string, origin?: string | URL) { if (!origin) { origin = new URL(self.__scramjet$config.codec.decode(location.href.slice((location.origin + self.__scramjet$config.prefix).length))); } + console.log(url); + if (url.startsWith("javascript:")) { return "javascript:" + rewriteJs(url.slice("javascript:".length)); - } else if (/^(#|mailto|about|data)/.test(url)) { + } else if (/^(#|mailto|about|data)/.test(url) || url.startsWith(location.origin + self.__scramjet$config.prefix)) { return url; } else if (canParseUrl(url, origin)) { return location.origin + self.__scramjet$config.prefix + self.__scramjet$config.codec.encode(new URL(url, origin).href); @@ -29,9 +31,7 @@ export function encodeUrl(url: string, origin?: URL) { export function decodeUrl(url: string) { if (/^(#|about|data|mailto|javascript)/.test(url)) { return url; - } else if (canParseUrl(url)) { - return self.__scramjet$config.codec.decode(url.slice((location.origin + self.__scramjet$config.prefix).length)) } else { - return url; + return self.__scramjet$config.codec.decode(url.slice((location.origin + self.__scramjet$config.prefix).length)) } } \ No newline at end of file diff --git a/src/client/beacon.ts b/src/client/beacon.ts index b42e173..bf377eb 100644 --- a/src/client/beacon.ts +++ b/src/client/beacon.ts @@ -1,6 +1,8 @@ +import { encodeUrl } from "../bundle"; + navigator.sendBeacon = new Proxy(navigator.sendBeacon, { apply(target, thisArg, argArray) { - argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]); + argArray[0] = encodeUrl(argArray[0]); return Reflect.apply(target, thisArg, argArray); }, diff --git a/src/client/css.ts b/src/client/css.ts index 34ec502..2e5e61b 100644 --- a/src/client/css.ts +++ b/src/client/css.ts @@ -1,21 +1,23 @@ +import { rewriteCss } from "../bundle"; + const cssProperties = ["background", "background-image", "mask", "mask-image", "list-style", "list-style-image", "border-image", "border-image-source", "cursor"]; const jsProperties = ["background", "backgroundImage", "mask", "maskImage", "listStyle", "listStyleImage", "borderImage", "borderImageSource", "cursor"]; - + CSSStyleDeclaration.prototype.setProperty = new Proxy(CSSStyleDeclaration.prototype.setProperty, { - apply(target, thisArg, argArray) { - if (cssProperties.includes(argArray[0])) argArray[1] = self.__scramjet$bundle.rewriters.rewriteCss(argArray[1]); - - return Reflect.apply(target, thisArg, argArray); - }, + apply(target, thisArg, argArray) { + if (cssProperties.includes(argArray[0])) argArray[1] = rewriteCss(argArray[1]); + + return Reflect.apply(target, thisArg, argArray); + }, }); jsProperties.forEach((prop) => { - const propDescriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, prop); + const propDescriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, prop); - Object.defineProperty(CSSStyleDeclaration.prototype, prop, { - set(v) { - propDescriptor.set.call(this, self.__scramjet$bundle.rewriters.rewriteCss(v)); - }, - }) + Object.defineProperty(CSSStyleDeclaration.prototype, prop, { + set(v) { + propDescriptor.set.call(this, rewriteCss(v)); + }, + }) }); diff --git a/src/client/element.ts b/src/client/element.ts index 31d9531..b99f688 100644 --- a/src/client/element.ts +++ b/src/client/element.ts @@ -1,3 +1,6 @@ +import { encodeUrl, rewriteCss, rewriteJs, rewriteSrcset } from "../bundle"; +import { rewriteHtml } from "../html"; + // object // iframe // embed @@ -37,25 +40,30 @@ Object.keys(attribs).forEach((attrib: string) => { const descriptor = Object.getOwnPropertyDescriptor(element.prototype, attrib); Object.defineProperty(element.prototype, attrib, { get() { - return descriptor.get.call(this, [this.dataset[`_${attrib}`]]); + return this.dataset[`${attrib}`]; }, set(value) { - this.dataset[`_${attrib}`] = value; + this.dataset[`${attrib}`] = value; if (/nonce|integrity|csp/.test(attrib)) { this.removeAttribute(attrib); } else if (/src|href|data|action|formaction/.test(attrib)) { + // @ts-expect-error + // TrustedScriptURL does not exist as a type yet, but it is a real thing if (value instanceof TrustedScriptURL) { return; } - value = self.__scramjet$bundle.rewriters.url.encodeUrl(value); + value = encodeUrl(value); } else if (attrib === "srcdoc") { - value = self.__scramjet$bundle.rewriters.rewriteHtml(value); + // @ts-ignore + // This needs to be ignored because I'm bad at TypeScript + + value = rewriteHtml(value).documentElement.innerHTML; } else if (/(image)?srcset/.test(attrib)) { - value = self.__scramjet$bundle.rewriters.rewriteSrcset(value); + value = rewriteSrcset(value); } else if (attrib === "style") { - value = self.__scramjet$bundle.rewriters.rewriteCss(value); + value = rewriteCss(value); } descriptor.set.call(this, value); @@ -64,34 +72,31 @@ Object.keys(attribs).forEach((attrib: string) => { }) }); -HTMLElement.prototype.getAttribute = new Proxy(Element.prototype.getAttribute, { +Element.prototype.getAttribute = new Proxy(Element.prototype.getAttribute, { apply(target, thisArg, argArray) { - console.log(thisArg); - if (Object.keys(attribs).includes(argArray[0])) { - argArray[0] = `_${argArray[0]}`; + if (Object.keys(attribs).includes(argArray[0]) && thisArg.dataset[`${argArray[0]}`]) { + return thisArg.dataset[`${argArray[0]}`]; } return Reflect.apply(target, thisArg, argArray); }, }); -// setAttribute proxy is currently broken - -HTMLElement.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, { +Element.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, { apply(target, thisArg, argArray) { if (Object.keys(attribs).includes(argArray[0])) { - thisArg.dataset[`_${argArray[0]}`] = argArray[1]; + thisArg.dataset[`${argArray[0]}`] = argArray[1]; if (/nonce|integrity|csp/.test(argArray[0])) { return; } else if (/src|href|data|action|formaction/.test(argArray[0])) { - console.log(thisArg); - argArray[1] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[1]); + argArray[1] = encodeUrl(argArray[1]); } else if (argArray[0] === "srcdoc") { - argArray[1] = self.__scramjet$bundle.rewriters.rewriteHtml(argArray[1]); + // @ts-ignore + argArray[1] = rewriteHtml(argArray[1]).documentElement.innerHTML; } else if (/(image)?srcset/.test(argArray[0])) { - argArray[1] = self.__scramjet$bundle.rewriters.rewriteSrcset(argArray[1]); + argArray[1] = rewriteSrcset(argArray[1]); } else if (argArray[1] === "style") { - argArray[1] = self.__scramjet$bundle.rewriters.rewriteCss(argArray[1]); + argArray[1] = rewriteCss(argArray[1]); } } @@ -104,17 +109,22 @@ const innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML" Object.defineProperty(HTMLElement.prototype, "innerHTML", { set(value) { if (this instanceof HTMLScriptElement) { + // @ts-expect-error + // TrustedScript does not exist as a type yet, but it is a real thing if (!(value instanceof TrustedScript)) { - value = self.__scramjet$bundle.rewriters.rewriteJs(value); + value = rewriteJs(value); } } else if (this instanceof HTMLStyleElement) { - value = self.__scramjet$bundle.rewriters.rewriteCss(value); + value = rewriteCss(value); } else { + // @ts-expect-error + // TrustedHTML does not exist as a type, but it is a real thing if (!(value instanceof TrustedHTML)) { - value = self.__scramjet$bundle.rewriters.rewriteHtml(value); + // @ts-ignore + value = rewriteHtml(value).documentElement.innerHTML; } } - return innerHTML.set.call(this, value); + innerHTML.set.call(this, value); }, -}) \ No newline at end of file +}); diff --git a/src/client/eval.ts b/src/client/eval.ts index 27dd6b1..c811304 100644 --- a/src/client/eval.ts +++ b/src/client/eval.ts @@ -1,16 +1,18 @@ +import { rewriteJs } from "../bundle"; + const FunctionProxy = new Proxy(Function, { construct(target, argArray) { if (argArray.length === 1) { - return Reflect.construct(target, self.__scramjet$bundle.rewriters.rewriteJs(argArray[0])); + return Reflect.construct(target, rewriteJs(argArray[0])); } else { - return Reflect.construct(target, self.__scramjet$bundle.rewriters.rewriteJs(argArray[argArray.length - 1])) + return Reflect.construct(target, rewriteJs(argArray[argArray.length - 1])) } }, apply(target, thisArg, argArray) { if (argArray.length === 1) { - return Reflect.apply(target, undefined, self.__scramjet$bundle.rewriters.rewriteJs(argArray[0])); + return Reflect.apply(target, undefined, rewriteJs(argArray[0])); } else { - return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), self.__scramjet$bundle.rewriters.rewriteJs(argArray[argArray.length - 1])]) + return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), rewriteJs(argArray[argArray.length - 1])]) } }, }); diff --git a/src/client/fetch.ts b/src/client/fetch.ts index ed40f1c..172e425 100644 --- a/src/client/fetch.ts +++ b/src/client/fetch.ts @@ -1,8 +1,12 @@ // ts throws an error if you dont do window.fetch +import { encodeUrl, rewriteHeaders } from "../bundle"; + window.fetch = new Proxy(window.fetch, { apply(target, thisArg, argArray) { - argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]); + console.log(argArray); + if (!(argArray[0] instanceof Request)) argArray[0] = encodeUrl(argArray[0]); + console.log(argArray); return Reflect.apply(target, thisArg, argArray); }, @@ -10,7 +14,7 @@ window.fetch = new Proxy(window.fetch, { Headers = new Proxy(Headers, { construct(target, argArray, newTarget) { - argArray[0] = self.__scramjet$bundle.rewriters.rewriteHeaders(argArray[0]); + argArray[0] = rewriteHeaders(argArray[0]); return Reflect.construct(target, argArray, newTarget); }, @@ -18,7 +22,7 @@ Headers = new Proxy(Headers, { Request = new Proxy(Request, { construct(target, argArray, newTarget) { - if (typeof argArray[0] === "string") argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]); + if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]); return Reflect.construct(target, argArray, newTarget); }, @@ -26,8 +30,8 @@ Request = new Proxy(Request, { Response.redirect = new Proxy(Response.redirect, { apply(target, thisArg, argArray) { - argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]); + argArray[0] = encodeUrl(argArray[0]); return Reflect.apply(target, thisArg, argArray); }, -}); \ No newline at end of file +}); diff --git a/src/client/index.ts b/src/client/index.ts index b2eaa43..c591b66 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -11,4 +11,4 @@ declare global { interface Window { __location: Location; } -} +} \ No newline at end of file diff --git a/src/client/location.ts b/src/client/location.ts index 67aac60..85554f6 100644 --- a/src/client/location.ts +++ b/src/client/location.ts @@ -1,10 +1,11 @@ // @ts-nocheck +import { encodeUrl, decodeUrl } from "../bundle"; function urlLocation() { - let loc = new URL(self.__scramjet$bundle.rewriters.url.decodeUrl(location.href)); - loc.assign = (url: string) => location.assign(self.__scramjet$bundle.rewriters.url.encodeUrl(url)); + const loc = new URL(decodeUrl(location.href)); + loc.assign = (url: string) => location.assign(encodeUrl(url)); loc.reload = () => location.reload(); - loc.replace = (url: string) => location.replace(self.__scramjet$bundle.rewriters.url.encodeUrl(url)); + loc.replace = (url: string) => location.replace(encodeUrl(url)); loc.toString = () => loc.href; return loc; @@ -20,7 +21,7 @@ export function LocationProxy() { set(obj, prop, value) { if (prop === "href") { - location.href = self.__scramjet$bundle.rewriters.url.encodeUrl(value); + location.href = encodeUrl(value); } else { loc[prop] = value; } diff --git a/src/client/storage.ts b/src/client/storage.ts index 59852dd..5953cea 100644 --- a/src/client/storage.ts +++ b/src/client/storage.ts @@ -7,12 +7,10 @@ function filterStorage(scope: Storage) { } function storageProxy(scope: Storage): Storage { - // sessionStorage isn't properly implemented currently, since everything is being stored in IDB - // const store = new IDBMap(window.__location.host); return new Proxy(scope, { - get(target, prop, receiver) { + get(target, prop) { switch (prop) { case "getItem": return (key: string) => { diff --git a/src/client/trustedTypes.ts b/src/client/trustedTypes.ts index 3806a97..2894a71 100644 --- a/src/client/trustedTypes.ts +++ b/src/client/trustedTypes.ts @@ -1,9 +1,14 @@ +import { encodeUrl, rewriteJs } from "../bundle"; +import { rewriteHtml } from "../html"; + +// @ts-expect-error +// trustedTypes isn't a type on the global scope yet trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, { apply(target, thisArg, argArray) { if (argArray[1].createHTML) { argArray[1].createHTML = new Proxy(argArray[1].createHTML, { apply(target1, thisArg1, argArray1) { - return self.__scramjet$bundle.rewriters.rewriteHtml(target1(...argArray1)); + return rewriteHtml(target1(...argArray1)).documentElement.innerHTML; }, }); } @@ -11,7 +16,7 @@ trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, { if (argArray[1].createScript) { argArray[1].createScript = new Proxy(argArray[1].createScript, { apply(target1, thisArg1, argArray1) { - return self.__scramjet$bundle.rewriters.rewriteJs(target1(...argArray1)); + return rewriteJs(target1(...argArray1)); }, }); } @@ -19,7 +24,7 @@ trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, { if (argArray[1].createScriptURL) { argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, { apply(target1, thisArg1, argArray1) { - return self.__scramjet$bundle.rewriters.url.encodeUrl(target1(...argArray1)) + return encodeUrl(target1(...argArray1)) }, }) } diff --git a/src/client/xmlhttprequest.ts b/src/client/xmlhttprequest.ts index 8422a08..bee9567 100644 --- a/src/client/xmlhttprequest.ts +++ b/src/client/xmlhttprequest.ts @@ -1,18 +1,20 @@ -XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, { - apply(target, thisArg, argArray) { - if (argArray[1]) argArray[1] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[1]); +import { encodeUrl, rewriteHeaders } from "../bundle"; - return Reflect.apply(target, thisArg, argArray); - }, +XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, { + apply(target, thisArg, argArray) { + if (argArray[1]) argArray[1] = encodeUrl(argArray[1]); + + return Reflect.apply(target, thisArg, argArray); + }, }); XMLHttpRequest.prototype.setRequestHeader = new Proxy(XMLHttpRequest.prototype.setRequestHeader, { - apply(target, thisArg, argArray) { - let headerObject = Object.fromEntries([argArray]); - headerObject = self.__scramjet$bundle.rewriters.rewriteHeaders(headerObject); + apply(target, thisArg, argArray) { + let headerObject = Object.fromEntries([argArray]); + headerObject = rewriteHeaders(headerObject); - argArray = Object.entries(headerObject)[0]; + argArray = Object.entries(headerObject)[0]; - return Reflect.apply(target, thisArg, argArray); - }, + return Reflect.apply(target, thisArg, argArray); + }, }); diff --git a/src/html/index.ts b/src/html/index.ts new file mode 100644 index 0000000..e40d0d8 --- /dev/null +++ b/src/html/index.ts @@ -0,0 +1,82 @@ +// this code needs to be bundled as a separate file due to when it is ran + +import { encodeUrl, rewriteCss, rewriteJs, rewriteSrcset } from "../bundle"; + +const parser = new DOMParser(); + +function parseHtml(html: string) { + return parser.parseFromString(html, "text/html"); +} + +function traverseParsedHtml(node: Element) { + for (const cspAttr of ["csp", "nonce", "integrity"]) { + if (node.hasAttribute(cspAttr)) { + node.setAttribute("data-" + cspAttr, node.getAttribute(cspAttr)); + node.removeAttribute(cspAttr); + } + } + + for (const urlAttr of ["src", "href", "data", "action"]) { + if (node.hasAttribute(urlAttr) && !node.hasAttribute("data-scramjet")) { + const url = node.getAttribute(urlAttr); + node.setAttribute("data-" + urlAttr, url) + node.setAttribute(urlAttr, encodeUrl(node.getAttribute(urlAttr))); + } + } + + for (const srcsetAttr of ["srcset", "imagesrcset"]) { + if (node.hasAttribute(srcsetAttr)) { + const srcset = node.getAttribute(srcsetAttr); + node.setAttribute("data-", srcset); + node.setAttribute(srcsetAttr, rewriteSrcset(srcset)); + } + } + + if (node.hasAttribute("srcdoc")) { + const srcdoc = node.getAttribute("srcdoc"); + node.setAttribute("data-srcdoc", srcdoc); + + const rewrittenSrcdoc = rewriteHtml(srcdoc); + node.setAttribute("srcdoc", rewrittenSrcdoc.documentElement.innerHTML); + } + + if (node instanceof HTMLScriptElement) { + if (node.hasAttribute("data-scramjet")) { + return; + } + node.innerHTML = rewriteJs(node.textContent); + } else if (node instanceof HTMLStyleElement) { + node.innerHTML = rewriteCss(node.textContent); + } else if (node instanceof HTMLHeadElement) { + // this array is reversed because it uses node.prepend() + for (const scramjetScript of ["client", "config", "codecs"]) { + const script = document.createElement("script"); + script.src = self.__scramjet$config[scramjetScript]; + script.setAttribute("data-scramjet", ""); + node.prepend(script); + } + } + + if (node.children) { + for (const child of node.children) { + traverseParsedHtml(child); + } + } +} + +export function rewriteHtml(html: string) { + const parsedHtml = parseHtml(html); + traverseParsedHtml(parsedHtml.documentElement); + + return parsedHtml; +} + +navigator.serviceWorker.ready.then(({ active }) => { + if (active) { + active.postMessage("rewriteHtml"); + } +}); + +navigator.serviceWorker.addEventListener("message", (message) => { + document.documentElement.replaceWith(rewriteHtml(message.data).documentElement); +}); \ No newline at end of file diff --git a/src/scramjet.config.ts b/src/scramjet.config.ts index ffc27fc..fd9d50a 100644 --- a/src/scramjet.config.ts +++ b/src/scramjet.config.ts @@ -10,6 +10,7 @@ declare global { worker: string; client: string; codecs: string; + html: string; } } } @@ -21,5 +22,6 @@ self.__scramjet$config = { bundle: "/scramjet.bundle.js", worker: "/scramjet.worker.js", client: "/scramjet.client.js", - codecs: "/scramjet.codecs.js" + codecs: "/scramjet.codecs.js", + html: "/scramjet.html.js" } \ No newline at end of file diff --git a/src/worker/index.ts b/src/worker/index.ts index f828b05..5b63278 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -1,5 +1,6 @@ import { BareClient } from "@mercuryworkshop/bare-mux"; -import { BareResponseFetch } from "@mercuryworkshop/bare-mux" +import { BareResponseFetch } from "@mercuryworkshop/bare-mux"; +import { encodeUrl, decodeUrl, rewriteCss, rewriteJs, rewriteHeaders } from "../bundle"; declare global { interface Window { @@ -7,9 +8,11 @@ declare global { } } -self.ScramjetServiceWorker = class ScramjetServiceWorker { +export class ScramjetServiceWorker { client: typeof BareClient.prototype; config: typeof self.__scramjet$config; + html: string; + constructor(config = self.__scramjet$config) { this.client = new BareClient(); if (!config.prefix) config.prefix = "/scramjet/"; @@ -25,11 +28,11 @@ self.ScramjetServiceWorker = class ScramjetServiceWorker { const urlParam = new URLSearchParams(new URL(request.url).search); if (urlParam.has("url")) { - return Response.redirect(self.__scramjet$bundle.rewriters.url.encodeUrl(urlParam.get("url"), new URL(urlParam.get("url")))) + return Response.redirect(encodeUrl(urlParam.get("url"), new URL(urlParam.get("url")))) } try { - const url = new URL(self.__scramjet$bundle.rewriters.url.decodeUrl(request.url)); + const url = new URL(decodeUrl(request.url)); const response: BareResponseFetch = await this.client.fetch(url, { method: request.method, @@ -42,18 +45,24 @@ self.ScramjetServiceWorker = class ScramjetServiceWorker { }); let responseBody; - const responseHeaders = self.__scramjet$bundle.rewriters.rewriteHeaders(response.rawHeaders, url); + const responseHeaders = rewriteHeaders(response.rawHeaders, url); if (response.body) { switch (request.destination) { case "iframe": case "document": - responseBody = self.__scramjet$bundle.rewriters.rewriteHtml(await response.text(), url); + responseBody = + ` +
+ ${["codecs", "config", "html"].map((script) => "").join("")} + + `; + this.html = await response.text(); break; case "script": - responseBody = self.__scramjet$bundle.rewriters.rewriteJs(await response.text(), url); + responseBody = rewriteJs(await response.text(), url); break; case "style": - responseBody = self.__scramjet$bundle.rewriters.rewriteCss(await response.text(), url); + responseBody = rewriteCss(await response.text(), url); break; case "sharedworker": break; @@ -104,7 +113,13 @@ self.ScramjetServiceWorker = class ScramjetServiceWorker { console.error(err); - return renderError(err, self.__scramjet$bundle.rewriters.url.decodeUrl(request.url)); + return renderError(err, decodeUrl(request.url)); + } + } + + async messageListener(message: MessageEvent) { + if (message.data === "rewriteHtml") { + message.source.postMessage(this.html); } } } diff --git a/static/sw.js b/static/sw.js index 655b310..af4db20 100644 --- a/static/sw.js +++ b/static/sw.js @@ -1,7 +1,6 @@ -importScripts("scramjet.codecs.js"); -importScripts("scramjet.config.js"); -importScripts( __scramjet$config.bundle || "scramjet.bundle.js") -importScripts( __scramjet$config.worker || "scramjet.worker.js"); +import { ScramjetServiceWorker } from "./scramjet.worker.js"; +import "./scramjet.codecs.js"; +import "./scramjet.config.js"; const scramjet = new ScramjetServiceWorker(); @@ -13,4 +12,8 @@ self.addEventListener("fetch", async (event) => { return await fetch(event.request); } })()) -}) \ No newline at end of file +}); + +self.addEventListener("message", async (message) => { + await scramjet.messageListener(message); +}); \ No newline at end of file diff --git a/static/ui.js b/static/ui.js index 1e533c4..8cbc298 100644 --- a/static/ui.js +++ b/static/ui.js @@ -1,5 +1,6 @@ navigator.serviceWorker.register("./sw.js", { - scope: __scramjet$config.prefix + scope: __scramjet$config.prefix, + type: "module" }) const connection = new BareMux.BareMuxConnection("/bare-mux-worker.js") const flex = css`display: flex;`;