mirror of
https://github.com/MercuryWorkshop/scramjet.git
synced 2025-05-16 07:30:02 -04:00
add license and client apis
This commit is contained in:
parent
ecff2aca52
commit
aac959936c
19 changed files with 1004 additions and 89 deletions
|
@ -3,7 +3,6 @@ import { rewriteCss } from "./rewriters/css";
|
|||
import { rewriteHtml, rewriteSrcset } from "./rewriters/html";
|
||||
import { rewriteJs } from "./rewriters/js";
|
||||
import { rewriteHeaders } from "./rewriters/headers";
|
||||
import * as idb from "idb-keyval";
|
||||
|
||||
export function isScramjetFile(src: string) {
|
||||
let bool = false;
|
||||
|
@ -25,7 +24,6 @@ const bundle = {
|
|||
rewriteJs,
|
||||
rewriteHeaders
|
||||
},
|
||||
idb,
|
||||
isScramjetFile
|
||||
}
|
||||
|
||||
|
|
|
@ -3,5 +3,5 @@ import { encodeUrl } from "./url"
|
|||
export function rewriteCss(css: string, origin?: URL) {
|
||||
css = css.replace(/(?<=url\("?'?)[^"'][\S]*[^"'](?="?'?\);?)/g, (match) => encodeUrl(match, origin));
|
||||
|
||||
return "/* intercepted by scramjet 🐳 */" + css;
|
||||
return css;
|
||||
}
|
|
@ -17,6 +17,8 @@ export function rewriteHtml(html: string, origin?: URL) {
|
|||
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;
|
||||
|
@ -35,16 +37,17 @@ function traverseParsedHtml(node, origin?: URL) {
|
|||
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") {
|
||||
const contentArray = node.attribs.content.split(";url=");
|
||||
contentArray[1] = encodeUrl(contentArray[1], origin);
|
||||
node.attribs.content = contentArray.join(";url=");
|
||||
} 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=");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,62 @@
|
|||
import { parse } from "meriyah";
|
||||
import { parseModule } from "meriyah";
|
||||
import { generate } from "astring";
|
||||
import { makeTraveler } from "astravel";
|
||||
import { encodeUrl } from "./url";
|
||||
|
||||
// i am a cat. i like to be petted. i like to be fed. i like to be
|
||||
|
||||
// js rewiter is NOT finished
|
||||
|
||||
// location
|
||||
// window
|
||||
// self
|
||||
// globalThis
|
||||
// this
|
||||
// top
|
||||
// parent
|
||||
|
||||
|
||||
export function rewriteJs(js: string, origin?: URL) {
|
||||
const ast = parse(js, {
|
||||
module: true
|
||||
});
|
||||
|
||||
const customTraveler = makeTraveler({
|
||||
ImportDeclaration: (node) => {
|
||||
node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
|
||||
ImportExpression: (node) => {
|
||||
node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
|
||||
ExportAllDeclaration: (node) => {
|
||||
node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
|
||||
ExportNamedDeclaration: (node) => {
|
||||
if (node.source) node.source.value = encodeUrl(node.source.value, origin);
|
||||
}
|
||||
});
|
||||
|
||||
customTraveler.go(ast);
|
||||
try {
|
||||
const ast = parseModule(js, {
|
||||
module: true,
|
||||
webcompat: true
|
||||
});
|
||||
|
||||
return generate(ast);
|
||||
// const identifierList = [
|
||||
// "window",
|
||||
// "self",
|
||||
// "globalThis",
|
||||
// "parent",
|
||||
// "top",
|
||||
// "location",
|
||||
// ""
|
||||
// ]
|
||||
|
||||
const customTraveler = makeTraveler({
|
||||
ImportDeclaration: (node) => {
|
||||
node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
|
||||
ImportExpression: (node) => {
|
||||
node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
|
||||
ExportAllDeclaration: (node) => {
|
||||
node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
|
||||
ExportNamedDeclaration: (node) => {
|
||||
if (node.source) node.source.value = encodeUrl(node.source.value, origin);
|
||||
},
|
||||
});
|
||||
|
||||
customTraveler.go(ast);
|
||||
|
||||
return generate(ast);
|
||||
} catch {
|
||||
console.log(js);
|
||||
|
||||
return js;
|
||||
}
|
||||
}
|
7
src/client/beacon.ts
Normal file
7
src/client/beacon.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
navigator.sendBeacon = new Proxy(navigator.sendBeacon, {
|
||||
apply(target, thisArg, argArray) {
|
||||
argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||
|
||||
return Reflect.apply(target, thisArg, argArray);
|
||||
},
|
||||
});
|
120
src/client/element.ts
Normal file
120
src/client/element.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
// object
|
||||
// iframe
|
||||
// embed
|
||||
// link
|
||||
// style
|
||||
// script
|
||||
// img
|
||||
// source
|
||||
// form
|
||||
// meta
|
||||
// area
|
||||
// base
|
||||
// body
|
||||
// input
|
||||
// audio
|
||||
// button
|
||||
// track
|
||||
// video
|
||||
|
||||
const attribs = {
|
||||
"nonce": [HTMLElement],
|
||||
"integrity": [HTMLScriptElement, HTMLLinkElement],
|
||||
"csp": [HTMLIFrameElement],
|
||||
"src": [HTMLImageElement, HTMLMediaElement, HTMLIFrameElement, HTMLEmbedElement, HTMLScriptElement],
|
||||
"href": [HTMLAnchorElement, HTMLLinkElement],
|
||||
"data": [HTMLObjectElement],
|
||||
"action": [HTMLFormElement],
|
||||
"formaction": [HTMLButtonElement, HTMLInputElement],
|
||||
"srcdoc": [HTMLIFrameElement],
|
||||
"srcset": [HTMLImageElement, HTMLSourceElement],
|
||||
"imagesrcset": [HTMLLinkElement],
|
||||
"style": [HTMLElement]
|
||||
}
|
||||
|
||||
Object.keys(attribs).forEach((attrib: string) => {
|
||||
attribs[attrib].forEach((element) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(element.prototype, attrib);
|
||||
Object.defineProperty(element.prototype, attrib, {
|
||||
get() {
|
||||
return descriptor.get.call(this, [this.dataset[`_${attrib}`]]);
|
||||
},
|
||||
|
||||
set(value) {
|
||||
this.dataset[`_${attrib}`] = value;
|
||||
if (/nonce|integrity|csp/.test(attrib)) {
|
||||
this.removeAttribute(attrib);
|
||||
} else if (/src|href|data|action|formaction/.test(attrib)) {
|
||||
if (value instanceof TrustedScriptURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = self.__scramjet$bundle.rewriters.url.encodeUrl(value);
|
||||
} else if (attrib === "srcdoc") {
|
||||
value = self.__scramjet$bundle.rewriters.rewriteHtml(value);
|
||||
} else if (/(image)?srcset/.test(attrib)) {
|
||||
value = self.__scramjet$bundle.rewriters.rewriteSrcset(value);
|
||||
} else if (attrib === "style") {
|
||||
value = self.__scramjet$bundle.rewriters.rewriteCss(value);
|
||||
}
|
||||
|
||||
descriptor.set.call(this, value);
|
||||
},
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
HTMLElement.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]}`;
|
||||
}
|
||||
|
||||
return Reflect.apply(target, thisArg, argArray);
|
||||
},
|
||||
});
|
||||
|
||||
// setAttribute proxy is currently broken
|
||||
|
||||
HTMLElement.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, {
|
||||
apply(target, thisArg, argArray) {
|
||||
if (Object.keys(attribs).includes(argArray[0])) {
|
||||
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]);
|
||||
} else if (argArray[0] === "srcdoc") {
|
||||
argArray[1] = self.__scramjet$bundle.rewriters.rewriteHtml(argArray[1]);
|
||||
} else if (/(image)?srcset/.test(argArray[0])) {
|
||||
argArray[1] = self.__scramjet$bundle.rewriters.rewriteSrcset(argArray[1]);
|
||||
} else if (argArray[1] === "style") {
|
||||
argArray[1] = self.__scramjet$bundle.rewriters.rewriteCss(argArray[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.apply(target, thisArg, argArray);
|
||||
},
|
||||
});
|
||||
|
||||
const innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML");
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, "innerHTML", {
|
||||
set(value) {
|
||||
if (this instanceof HTMLScriptElement) {
|
||||
if (!(value instanceof TrustedScript)) {
|
||||
value = self.__scramjet$bundle.rewriters.rewriteJs(value);
|
||||
}
|
||||
} else if (this instanceof HTMLStyleElement) {
|
||||
value = self.__scramjet$bundle.rewriters.rewriteCss(value);
|
||||
} else {
|
||||
if (!(value instanceof TrustedHTML)) {
|
||||
value = self.__scramjet$bundle.rewriters.rewriteHtml(value);
|
||||
}
|
||||
}
|
||||
|
||||
return innerHTML.set.call(this, value);
|
||||
},
|
||||
})
|
26
src/client/eval.ts
Normal file
26
src/client/eval.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
const FunctionProxy = new Proxy(Function, {
|
||||
construct(target, argArray) {
|
||||
if (argArray.length === 1) {
|
||||
return Reflect.construct(target, self.__scramjet$bundle.rewriters.rewriteJs(argArray[0]));
|
||||
} else {
|
||||
return Reflect.construct(target, self.__scramjet$bundle.rewriters.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]));
|
||||
} else {
|
||||
return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), self.__scramjet$bundle.rewriters.rewriteJs(argArray[argArray.length - 1])])
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
delete window.Function;
|
||||
|
||||
window.Function = FunctionProxy;
|
||||
|
||||
delete window.eval;
|
||||
|
||||
// since the function proxy is already rewriting the js we can just reuse it for the eval proxy
|
||||
|
||||
window.eval = (str: string) => window.Function(str);
|
33
src/client/fetch.ts
Normal file
33
src/client/fetch.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// ts throws an error if you dont do window.fetch
|
||||
|
||||
window.fetch = new Proxy(window.fetch, {
|
||||
apply(target, thisArg, argArray) {
|
||||
argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||
|
||||
return Reflect.apply(target, thisArg, argArray);
|
||||
},
|
||||
});
|
||||
|
||||
Headers = new Proxy(Headers, {
|
||||
construct(target, argArray, newTarget) {
|
||||
argArray[0] = self.__scramjet$bundle.rewriters.rewriteHeaders(argArray[0]);
|
||||
|
||||
return Reflect.construct(target, argArray, newTarget);
|
||||
},
|
||||
})
|
||||
|
||||
Request = new Proxy(Request, {
|
||||
construct(target, argArray, newTarget) {
|
||||
if (typeof argArray[0] === "string") argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||
|
||||
return Reflect.construct(target, argArray, newTarget);
|
||||
},
|
||||
});
|
||||
|
||||
Response.redirect = new Proxy(Response.redirect, {
|
||||
apply(target, thisArg, argArray) {
|
||||
argArray[0] = self.__scramjet$bundle.rewriters.url.encodeUrl(argArray[0]);
|
||||
|
||||
return Reflect.apply(target, thisArg, argArray);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
import "./location";
|
||||
import "./storage";
|
||||
import "./element.ts";
|
||||
import "./eval.ts";
|
||||
import "./fetch.ts";
|
||||
import "./trustedTypes.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @ts-nocheck
|
||||
|
||||
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));
|
||||
|
@ -8,13 +10,14 @@ function urlLocation() {
|
|||
return loc;
|
||||
}
|
||||
|
||||
export function locationProxy() {
|
||||
export function LocationProxy() {
|
||||
const loc = urlLocation();
|
||||
|
||||
return new Proxy(Location, {
|
||||
return new Proxy(window.location, {
|
||||
get(target, prop) {
|
||||
return loc[prop];
|
||||
},
|
||||
|
||||
set(obj, prop, value) {
|
||||
if (prop === "href") {
|
||||
location.href = self.__scramjet$bundle.rewriters.url.encodeUrl(value);
|
||||
|
@ -27,4 +30,4 @@ export function locationProxy() {
|
|||
})
|
||||
}
|
||||
|
||||
window.__location = locationProxy();
|
||||
window.__location = LocationProxy();
|
|
@ -1,46 +1,55 @@
|
|||
// import IDBMap from "@webreflection/idb-map";
|
||||
|
||||
// this will be converted to use IDB later but i can't figure out how to make it work synchronously
|
||||
|
||||
function filterStorage(scope: Storage) {
|
||||
return Object.keys(scope).filter((key) => key.startsWith(window.__location.host));
|
||||
}
|
||||
|
||||
function storageProxy(scope: Storage): Storage {
|
||||
// sessionStorage isn't properly implemented currently, since everything is being stored in IDB
|
||||
|
||||
const { set, get, keys, del, createStore } = self.__scramjet$bundle.idb;
|
||||
const store = createStore(window.__location.host, "store");
|
||||
// const store = new IDBMap(window.__location.host);
|
||||
|
||||
return new Proxy(scope, {
|
||||
get(target, prop) {
|
||||
get(target, prop, receiver) {
|
||||
switch (prop) {
|
||||
case "getItem":
|
||||
return async function getItem(key: string) {
|
||||
return await get(key, store);
|
||||
return (key: string) => {
|
||||
return target.getItem(window.__location.host + "@" + key);
|
||||
}
|
||||
|
||||
case "setItem":
|
||||
return async function setItem(key: string, value: string) {
|
||||
await set(key, value, store);
|
||||
return (key: string, value: string) => {
|
||||
target.setItem(window.__location.host + "@" + key, value);
|
||||
}
|
||||
|
||||
|
||||
case "removeItem":
|
||||
return async function removeItem(key: string) {
|
||||
await del(key, store);
|
||||
return (key: string) => {
|
||||
target.removeItem(window.__location.host + "@" + key);
|
||||
}
|
||||
|
||||
case "clear":
|
||||
return async function clear() {
|
||||
// clear can't be used because the names are the exact same
|
||||
await self.__scramjet$bundle.idb.clear(store);
|
||||
return () => {
|
||||
filterStorage(target).forEach((key) => target.removeItem(key));
|
||||
}
|
||||
|
||||
case "key":
|
||||
return async function key(index: number) {
|
||||
// supposed to be key but key is the name of the function
|
||||
return (await keys(store))[index];
|
||||
return (index: number) => {
|
||||
return target[filterStorage(target)[index]];
|
||||
}
|
||||
|
||||
case "length":
|
||||
return (async ()=>{
|
||||
return (await keys(store)).length;
|
||||
})();
|
||||
return filterStorage(target).length;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
defineProperty(target, property, attributes) {
|
||||
target.setItem(property as string, attributes.value);
|
||||
|
||||
return true;
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const localStorageProxy = storageProxy(window.localStorage);
|
||||
|
|
29
src/client/trustedTypes.ts
Normal file
29
src/client/trustedTypes.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
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));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (argArray[1].createScript) {
|
||||
argArray[1].createScript = new Proxy(argArray[1].createScript, {
|
||||
apply(target1, thisArg1, argArray1) {
|
||||
return self.__scramjet$bundle.rewriters.rewriteJs(target1(...argArray1));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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 Reflect.apply(target, thisArg, argArray);
|
||||
},
|
||||
})
|
|
@ -22,6 +22,12 @@ self.ScramjetServiceWorker = class ScramjetServiceWorker {
|
|||
}
|
||||
|
||||
async fetch({ request }: FetchEvent) {
|
||||
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"))))
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(self.__scramjet$bundle.rewriters.url.decodeUrl(request.url));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue