add license and client apis

This commit is contained in:
Avad3 2024-06-17 06:20:32 -04:00
parent ecff2aca52
commit aac959936c
19 changed files with 1004 additions and 89 deletions

View file

@ -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
}

View file

@ -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;
}

View file

@ -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=");
}
}

View file

@ -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
View 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
View 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
View 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
View 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);
},
});

View file

@ -1,5 +1,9 @@
import "./location";
import "./storage";
import "./element.ts";
import "./eval.ts";
import "./fetch.ts";
import "./trustedTypes.ts";
declare global {
interface Window {

View file

@ -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();

View file

View file

@ -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);

View 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);
},
})

View file

@ -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));