Merge pull request #11 from MercuryWorkshop/globals

Globals
This commit is contained in:
Toshit 2024-07-14 17:03:37 -07:00 committed by GitHub
commit a914e9a933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 8085 additions and 3028 deletions

View file

@ -1,28 +1,30 @@
{ {
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint"],
"rules": { "rules": {
"no-await-in-loop": "warn", "no-await-in-loop": "warn",
"no-unused-labels": "error", "no-unused-labels": "error",
"no-unused-vars": "error", "no-unused-vars": "error",
"quotes": ["error", "double"], "quotes": ["error", "double"],
"max-lines-per-function": ["error", { "max-lines-per-function": [
"max": 200, "error",
"skipComments": true {
}], "max": 200,
"getter-return": "error", "skipComments": true
"newline-before-return": "error", }
"no-multiple-empty-lines": "error", ],
"no-var": "error", "getter-return": "error",
"indent": ["warn", 4], "newline-before-return": "error",
"no-this-before-super": "warn", "no-multiple-empty-lines": "error",
"no-useless-return": "error", "no-var": "error",
"no-shadow": "error", "no-this-before-super": "warn",
"prefer-const": "warn", "no-useless-return": "error",
"no-unreachable": "warn", "no-shadow": "error",
"no-undef": "off", "prefer-const": "warn",
"@typescript-eslint/no-explicit-any": "off", "no-unreachable": "warn",
"@typescript-eslint/ban-ts-comment": "off" "no-undef": "off",
} "@typescript-eslint/no-explicit-any": "off",
} "@typescript-eslint/ban-ts-comment": "off"
}
}

View file

@ -1,22 +1,22 @@
# Scramjet # Scramjet
Scramjet is an experimental web proxy that aims to be the successor to Ultraviolet. Scramjet is an experimental web proxy that aims to be the successor to Ultraviolet.
It currently does not support most websites due to it being very early in the development stage. It currently does not support most websites due to it being very early in the development stage.
The UI is not finalized and only used as a means to test the web proxy. The UI is not finalized and only used as a means to test the web proxy.
## How to build ## How to build
Running `pnpm dev` will build Scramjet and start a dev server on localhost:1337. If you only want to build the proxy without using the dev server, run `pnpm build`.
Running `pnpm dev` will build Scramjet and start a dev server on localhost:1337. If you only want to build the proxy without using the dev server, run `pnpm build`.
## TODO
## TODO
- Finish HTML rewriting - Finish HTML rewriting
- `<script type="importmap"></script>` rewriting - `<script type="importmap"></script>` rewriting
- Make an array of all possible import values and pass the array onto the JS rewriter, then rewrite all the URLs inside of it - Make an array of all possible import values and pass the array onto the JS rewriter, then rewrite all the URLs inside of it
- Finish JS rewriting - Finish JS rewriting
- Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present - Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present
- Write client APIs - Write client APIs
- Fix `Illegal Invocation` when calling `addEventListener()` on the window proxy - Fix `Illegal Invocation` when calling `addEventListener()` on the window proxy
- Get rid of ESM builds and pollute the global namespace (maybe?) - Get rid of ESM builds and pollute the global namespace (maybe?)

View file

@ -1,7 +1,7 @@
"use strict"; "use strict";
const { resolve } = require("node:path"); const { resolve } = require("node:path");
const scramjetPath = resolve(__dirname, "..", "dist"); const scramjetPath = resolve(__dirname, "..", "dist");
exports.scramjetPath = scramjetPath; exports.scramjetPath = scramjetPath;

6
lib/index.d.ts vendored
View file

@ -1,3 +1,3 @@
declare const scramjetPath: string; declare const scramjetPath: string;
export { scramjetPath }; export { scramjetPath };

View file

@ -1,60 +1,59 @@
{ {
"name": "@mercuryworkshop/scramjet", "name": "@mercuryworkshop/scramjet",
"version": "1.0.2", "version": "1.0.2",
"description": "An experimental web proxy that aims to be the successor to Ultraviolet", "description": "An experimental web proxy that aims to be the successor to Ultraviolet",
"main": "./lib/index.cjs", "main": "./lib/index.cjs",
"types": "./lib/index.d.js", "types": "./lib/index.d.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/MercuryWorkshop/scramjet" "url": "https://github.com/MercuryWorkshop/scramjet"
}, },
"scripts": { "scripts": {
"build": "rollup -c", "build": "rspack build",
"dev": "node server.js", "dev": "node server.js",
"temp": "rollup -c -w", "prepublish": "pnpm build",
"prepublish": "pnpm build", "pub": "pnpm publish --no-git-checks --access public"
"pub": "pnpm publish --no-git-checks --access public" },
}, "files": [
"files": [ "dist",
"dist", "lib"
"lib" ],
], "keywords": [],
"keywords": [], "author": "",
"author": "", "license": "ISC",
"license": "ISC", "devDependencies": {
"devDependencies": { "@fastify/static": "^7.0.3",
"@fastify/static": "^7.0.3", "@mercuryworkshop/bare-as-module3": "^2.2.2",
"@mercuryworkshop/bare-as-module3": "^2.2.2", "@mercuryworkshop/epoxy-transport": "^2.1.3",
"@mercuryworkshop/epoxy-transport": "^2.1.3", "@mercuryworkshop/libcurl-transport": "^1.3.6",
"@mercuryworkshop/libcurl-transport": "^1.3.6", "@rsdoctor/rspack-plugin": "^0.3.7",
"@rollup/plugin-inject": "^5.0.5", "@rspack/cli": "^0.7.5",
"@rollup/plugin-replace": "^5.0.5", "@rspack/core": "^0.7.5",
"@rollup/plugin-node-resolve": "^15.2.3", "@tomphttp/bare-server-node": "^2.0.3",
"@tomphttp/bare-server-node": "^2.0.3", "@types/eslint": "^8.56.10",
"@types/eslint": "^8.56.10", "@types/estree": "^1.0.5",
"@types/estree": "^1.0.5", "@types/node": "^20.14.10",
"@types/node": "^20.14.10", "@types/serviceworker": "^0.0.85",
"@types/serviceworker": "^0.0.85", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "dotenv": "^16.4.5",
"dotenv": "^16.4.5", "eslint": "^8.57.0",
"eslint": "^8.57.0", "fastify": "^4.26.2",
"fastify": "^4.26.2", "prettier": "^3.3.3",
"rollup": "^4.17.2", "tslib": "^2.6.2",
"rollup-plugin-typescript2": "^0.36.0", "typescript": "^5.4.5"
"tslib": "^2.6.2", },
"typescript": "^5.4.5" "type": "module",
}, "dependencies": {
"type": "module", "@mercuryworkshop/bare-mux": "^2.0.2",
"dependencies": { "@webreflection/idb-map": "^0.3.1",
"@mercuryworkshop/bare-mux": "^2.0.2", "astravel": "^0.6.1",
"@webreflection/idb-map": "^0.3.1", "astring": "^1.8.6",
"astravel": "^0.6.1", "dom-serializer": "^2.0.0",
"astring": "^1.8.6", "domhandler": "^5.0.3",
"dom-serializer": "^2.0.0", "domutils": "^3.1.0",
"domhandler": "^5.0.3", "htmlparser2": "^9.1.0",
"domutils": "^3.1.0", "meriyah": "^4.4.2",
"htmlparser2": "^9.1.0", "parse-domain": "^8.0.2"
"meriyah": "^4.4.2" }
}
} }

6331
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

6
prettier.json Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"useTabs": true,
"semi": true,
"singleQuote": false
}

View file

@ -1,34 +0,0 @@
import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from '@rollup/plugin-node-resolve';
import { join } from "node:path";
import fs from "node:fs"
import { fileURLToPath } from "node:url";
// check if its
const production = !process.env.ROLLUP_WATCH;
console.log(production)
fs.rmSync(join(fileURLToPath(new URL(".", import.meta.url)), "./dist"), { recursive: true, force: true })
const commonPlugins = () => [
typescript(),
nodeResolve(),
]
export default {
plugins: commonPlugins(),
treeshake: "recommended",
input: {
client: "./src/client/index.ts",
shared: "./src/shared/index.ts",
worker: "./src/worker/index.ts",
codecs: "./src/codecs/index.ts",
config: "./src/scramjet.config.ts"
},
output: {
entryFileNames: "scramjet.[name].js",
dir: "./dist",
format: "esm",
sourcemap: true,
compact: production,
},
};

53
rspack.config.js Normal file
View file

@ -0,0 +1,53 @@
import { defineConfig } from "@rspack/cli";
// import { RsdoctorRspackPlugin } from "@rsdoctor/rspack-plugin";
import { join } from "path";
import { fileURLToPath } from "url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
export default defineConfig({
// change to production when needed
mode: "development",
entry: {
shared: join(__dirname, "src/shared/index.ts"),
worker: join(__dirname, "src/worker/index.ts"),
client: join(__dirname, "src/client/index.ts"),
config: join(__dirname, "src/scramjet.config.ts"),
codecs: join(__dirname, "src/codecs/index.ts"),
},
resolve: {
extensions: [".ts", ".js"],
},
module: {
rules: [
{
test: /\.ts$/,
use: "builtin:swc-loader",
exclude: ["/node_modules/"],
options: {
jsc: {
parser: {
syntax: "typescript",
},
},
},
type: "javascript/auto",
},
],
},
output: {
filename: "scramjet.[name].js",
path: join(__dirname, "dist"),
iife: true,
clean: true,
},
plugins: [
// new RsdoctorRspackPlugin({
// supports: {
// parseBundle: true,
// banner: true
// }
// })
],
watch: true,
});

166
server.js
View file

@ -1,82 +1,84 @@
// Dev server imports // Dev server imports
import { createBareServer } from "@tomphttp/bare-server-node"; import { createBareServer } from "@tomphttp/bare-server-node";
import { createServer } from "http"; import { createServer } from "http";
import Fastify from "fastify"; import Fastify from "fastify";
import fastifyStatic from "@fastify/static"; import fastifyStatic from "@fastify/static";
import { join } from "node:path"; import { join } from "node:path";
import fs from "node:fs" import { spawn } from "node:child_process";
import { spawn } from "node:child_process" import { fileURLToPath } from "node:url";
import { fileURLToPath } from "node:url";
import { loadConfigFile } from "rollup/loadConfigFile" //transports
import { baremuxPath } from "@mercuryworkshop/bare-mux/node";
//transports import { epoxyPath } from "@mercuryworkshop/epoxy-transport";
import { baremuxPath } from "@mercuryworkshop/bare-mux/node" import { libcurlPath } from "@mercuryworkshop/libcurl-transport";
import { epoxyPath } from "@mercuryworkshop/epoxy-transport" import { bareModulePath } from "@mercuryworkshop/bare-as-module3";
import { libcurlPath } from "@mercuryworkshop/libcurl-transport"
import { bareModulePath } from "@mercuryworkshop/bare-as-module3" const bare = createBareServer("/bare/", {
logErrors: true,
const bare = createBareServer("/bare/", { });
logErrors: true
}); const fastify = Fastify({
serverFactory: (handler) => {
const fastify = Fastify({ return createServer()
serverFactory: (handler) => { .on("request", (req, res) => {
return createServer() if (bare.shouldRoute(req)) {
.on("request", (req, res) => { bare.routeRequest(req, res);
if (bare.shouldRoute(req)) { } else {
bare.routeRequest(req, res); handler(req, res);
} else { }
handler(req, res); })
} .on("upgrade", (req, socket, head) => {
}).on("upgrade", (req, socket, head) => { if (bare.shouldRoute(req)) {
if (bare.shouldRoute(req)) { bare.routeUpgrade(req, socket, head);
bare.routeUpgrade(req, socket, head); } else {
} else { socket.end();
socket.end(); }
} });
}) },
} });
});
fastify.register(fastifyStatic, {
fastify.register(fastifyStatic, { root: join(fileURLToPath(new URL(".", import.meta.url)), "./static"),
root: join(fileURLToPath(new URL(".", import.meta.url)), "./static"), decorateReply: false,
decorateReply: false });
}); fastify.register(fastifyStatic, {
fastify.register(fastifyStatic, { root: join(fileURLToPath(new URL(".", import.meta.url)), "./dist"),
root: join(fileURLToPath(new URL(".", import.meta.url)), "./dist"), prefix: "/scram/",
prefix: "/scram/", decorateReply: false,
decorateReply: false });
}) fastify.register(fastifyStatic, {
fastify.register(fastifyStatic, { root: baremuxPath,
root: baremuxPath, prefix: "/baremux/",
prefix: "/baremux/", decorateReply: false,
decorateReply: false });
}) fastify.register(fastifyStatic, {
fastify.register(fastifyStatic, { root: epoxyPath,
root: epoxyPath, prefix: "/epoxy/",
prefix: "/epoxy/", decorateReply: false,
decorateReply: false });
}) fastify.register(fastifyStatic, {
fastify.register(fastifyStatic, { root: libcurlPath,
root: libcurlPath, prefix: "/libcurl/",
prefix: "/libcurl/", decorateReply: false,
decorateReply: false });
}) fastify.register(fastifyStatic, {
fastify.register(fastifyStatic, { root: bareModulePath,
root: bareModulePath, prefix: "/baremod/",
prefix: "/baremod/", decorateReply: false,
decorateReply: false });
}) fastify.listen({
fastify.listen({ port: process.env.PORT || 1337,
port: process.env.PORT || 1337 });
});
const watch = spawn("pnpm", ["rspack", "-w"], {
const watch = spawn("rollup", ["-c", "-w"], { detached: true }); detached: true,
cwd: process.cwd(),
watch.stdout.on("data", (data) => { });
console.log(`${data}`);
}); watch.stdout.on("data", (data) => {
console.log(`${data}`);
watch.stderr.on("data", (data) => { });
console.log(`${data}`);
}); watch.stderr.on("data", (data) => {
console.log(`${data}`);
});

View file

@ -1,9 +1,9 @@
import { encodeUrl } from "../shared"; import { encodeUrl } from "./shared";
navigator.sendBeacon = new Proxy(navigator.sendBeacon, { navigator.sendBeacon = new Proxy(navigator.sendBeacon, {
apply(target, thisArg, argArray) { apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]); argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray); return Reflect.apply(target, thisArg, argArray);
}, },
}); });

View file

@ -1,26 +1,26 @@
import { rewriteCss } from "../shared"; import { rewriteCss } from "./shared";
const cssProperties = ["background", "background-image", "mask", "mask-image", "list-style", "list-style-image", "border-image", "border-image-source", "cursor"]; const cssProperties = [
const jsProperties = ["background", "backgroundImage", "mask", "maskImage", "listStyle", "listStyleImage", "borderImage", "borderImageSource", "cursor"]; "background",
"background-image",
"mask",
CSSStyleDeclaration.prototype.setProperty = new Proxy(CSSStyleDeclaration.prototype.setProperty, { "mask-image",
apply(target, thisArg, argArray) { "list-style",
if (cssProperties.includes(argArray[0])) argArray[1] = rewriteCss(argArray[1]); "list-style-image",
"border-image",
return Reflect.apply(target, thisArg, argArray); "border-image-source",
}, "cursor",
}); ];
// const jsProperties = ["background", "backgroundImage", "mask", "maskImage", "listStyle", "listStyleImage", "borderImage", "borderImageSource", "cursor"];
jsProperties.forEach((prop) => {
const propDescriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, prop); CSSStyleDeclaration.prototype.setProperty = new Proxy(
CSSStyleDeclaration.prototype.setProperty,
Object.defineProperty(CSSStyleDeclaration.prototype, prop, { {
get() { apply(target, thisArg, argArray) {
return propDescriptor.get.call(this); if (cssProperties.includes(argArray[0]))
}, argArray[1] = rewriteCss(argArray[1]);
set(v) {
return propDescriptor.set.call(this, rewriteCss(v)); return Reflect.apply(target, thisArg, argArray);
}, },
}) }
}); );

View file

@ -1,98 +1,123 @@
import { encodeUrl, rewriteCss, rewriteHtml, rewriteJs, rewriteSrcset } from "../shared"; import { decodeUrl } from "../shared/rewriters/url";
import {
const attrObject = { encodeUrl,
"nonce": [HTMLElement], rewriteCss,
"integrity": [HTMLScriptElement, HTMLLinkElement], rewriteHtml,
"csp": [HTMLIFrameElement], rewriteJs,
"src": [HTMLImageElement, HTMLMediaElement, HTMLIFrameElement, HTMLEmbedElement, HTMLScriptElement], rewriteSrcset,
"href": [HTMLAnchorElement, HTMLLinkElement], } from "./shared";
"data": [HTMLObjectElement],
"action": [HTMLFormElement], const attrObject = {
"formaction": [HTMLButtonElement, HTMLInputElement], nonce: [HTMLElement],
"srcdoc": [HTMLIFrameElement], integrity: [HTMLScriptElement, HTMLLinkElement],
"srcset": [HTMLImageElement, HTMLSourceElement], csp: [HTMLIFrameElement],
"imagesrcset": [HTMLLinkElement] src: [
} HTMLImageElement,
HTMLMediaElement,
const attrs = Object.keys(attrObject); HTMLIFrameElement,
HTMLEmbedElement,
for (const attr of attrs) { HTMLScriptElement,
for (const element of attrObject[attr]) { ],
const descriptor = Object.getOwnPropertyDescriptor(element.prototype, attr); href: [HTMLAnchorElement, HTMLLinkElement],
Object.defineProperty(element.prototype, attr, { data: [HTMLObjectElement],
get() { action: [HTMLFormElement],
return this.dataset[attr]; formaction: [HTMLButtonElement, HTMLInputElement],
}, srcdoc: [HTMLIFrameElement],
srcset: [HTMLImageElement, HTMLSourceElement],
set(value) { imagesrcset: [HTMLLinkElement],
this.dataset[attr] = value; };
if (/nonce|integrity|csp/.test(attr)) {
return; const attrs = Object.keys(attrObject);
} else if (/src|href|data|action|formaction/.test(attr)) {
// @ts-expect-error for (const attr of attrs) {
if (value instanceof TrustedScriptURL) { for (const element of attrObject[attr]) {
return; const descriptor = Object.getOwnPropertyDescriptor(element.prototype, attr);
} Object.defineProperty(element.prototype, attr, {
get() {
value = encodeUrl(value); if (/src|href|data|action|formaction/.test(attr)) {
} else if (attr === "srcdoc") { return decodeUrl(descriptor.get.call(this));
value = rewriteHtml(value); }
} else if (/(image)?srcset/.test(attr)) {
value = rewriteSrcset(value); if (this.__origattrs[attr]) {
} return this.__origattrs[attr];
}
descriptor.set.call(this, value);
}, return descriptor.get.call(this);
}); },
}
} set(value) {
this.__origattrs[attr] = value;
Element.prototype.getAttribute = new Proxy(Element.prototype.getAttribute, {
apply(target, thisArg, argArray) { if (/nonce|integrity|csp/.test(attr)) {
if (attrs.includes(argArray[0]) && thisArg.dataset[argArray[0]]) { return;
return thisArg.dataset[argArray[0]]; } else if (/src|href|data|action|formaction/.test(attr)) {
} value = encodeUrl(value);
} else if (attr === "srcdoc") {
return Reflect.apply(target, thisArg, argArray); value = rewriteHtml(value);
}, } else if (/(image)?srcset/.test(attr)) {
}); value = rewriteSrcset(value);
}
Element.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, {
apply(target, thisArg, argArray) { descriptor.set.call(this, value);
if (attrs.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); declare global {
argArray[1] = encodeUrl(argArray[1]); interface Element {
} else if (argArray[0] === "srcdoc") { __origattrs: Record<string, string>;
argArray[1] = rewriteHtml(argArray[1]); }
} else if (/(image)?srcset/.test(argArray[0])) { }
argArray[1] = rewriteSrcset(argArray[1]);
} else if (argArray[1] === "style") { Element.prototype.__origattrs = {};
argArray[1] = rewriteCss(argArray[1]);
} Element.prototype.getAttribute = new Proxy(Element.prototype.getAttribute, {
} apply(target, thisArg, argArray) {
if (attrs.includes(argArray[0]) && thisArg.__origattrs[argArray[0]]) {
return Reflect.apply(target, thisArg, argArray); return thisArg.__origattrs[argArray[0]];
}, }
});
return Reflect.apply(target, thisArg, argArray);
const innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML"); },
});
Object.defineProperty(Element.prototype, "innerHTML", {
set(value) { Element.prototype.setAttribute = new Proxy(Element.prototype.setAttribute, {
// @ts-expect-error apply(target, thisArg, argArray) {
if (this instanceof HTMLScriptElement && !(value instanceof TrustedScript)) { if (attrs.includes(argArray[0])) {
value = rewriteJs(value); thisArg.__origattrs[argArray[0]] = argArray[1];
} else if (this instanceof HTMLStyleElement) { if (/nonce|integrity|csp/.test(argArray[0])) {
value = rewriteCss(value); return;
// @ts-expect-error } else if (/src|href|data|action|formaction/.test(argArray[0])) {
} else if (!(value instanceof TrustedHTML)) { argArray[1] = encodeUrl(argArray[1]);
value = rewriteHtml(value); } else if (argArray[0] === "srcdoc") {
} argArray[1] = rewriteHtml(argArray[1]);
} else if (/(image)?srcset/.test(argArray[0])) {
return innerHTML.set.call(this, value); argArray[1] = rewriteSrcset(argArray[1]);
}, } else if (argArray[1] === "style") {
}) argArray[1] = rewriteCss(argArray[1]);
}
}
return Reflect.apply(target, thisArg, argArray);
},
});
const innerHTML = Object.getOwnPropertyDescriptor(
Element.prototype,
"innerHTML"
);
Object.defineProperty(Element.prototype, "innerHTML", {
set(value) {
if (
this instanceof HTMLScriptElement
) {
value = rewriteJs(value);
} else if (this instanceof HTMLStyleElement) {
value = rewriteCss(value);
}
return innerHTML.set.call(this, value);
},
});

View file

@ -4,7 +4,7 @@
// window.addEventListener = new Proxy(window.addEventListener, { // window.addEventListener = new Proxy(window.addEventListener, {
// apply (target, thisArg, argArray) { // apply (target, thisArg, argArray) {
// // // //
// return Reflect.apply(target, thisArg, argArray); // return Reflect.apply(target, thisArg, argArray);
// } // }
// }) // })

View file

@ -1,19 +1,17 @@
import { decodeUrl } from "./shared";
import { encodeUrl } from "../shared";
window.history.pushState = new Proxy(window.history.pushState, {
window.history.pushState = new Proxy(window.history.pushState, { apply(target, thisArg, argArray) {
apply(target, thisArg, argArray) { argArray[3] = decodeUrl(argArray[3]);
argArray[3] = encodeUrl(argArray[3]);
return Reflect.apply(target, thisArg, argArray);
return Reflect.apply(target, thisArg, argArray); },
}, });
});
window.history.replaceState = new Proxy(window.history.replaceState, {
apply(target, thisArg, argArray) {
window.history.replaceState = new Proxy(window.history.replaceState, { argArray[3] = decodeUrl(argArray[3]);
apply(target, thisArg, argArray) {
argArray[3] = encodeUrl(argArray[3]); return Reflect.apply(target, thisArg, argArray);
},
return Reflect.apply(target, thisArg, argArray); });
},
});

View file

@ -1,38 +1,21 @@
import "./window.ts"; import "./scope.ts";
import "./event.ts"; import "./window.ts";
import "./native/eval.ts"; import "./event.ts";
import "./location.ts"; import "./native/eval.ts";
import "./trustedTypes.ts"; import "./location.ts";
import "./requests/fetch.ts"; import "./trustedTypes.ts";
import "./requests/xmlhttprequest.ts"; import "./requests/fetch.ts";
import "./requests/websocket.ts" import "./requests/xmlhttprequest.ts";
import "./element.ts"; import "./requests/websocket.ts";
import "./storage.ts"; import "./element.ts";
import "./css.ts"; import "./storage.ts";
import "./history.ts" import "./css.ts";
import "./worker.ts"; import "./history.ts";
import "./scope.ts"; import "./worker.ts";
import "./url.ts";
declare global {
interface Window { declare global {
//@ts-ignore scope function cant be typed interface Window {
__s: any; $s: any;
} }
} }
const scripts = document.querySelectorAll("script:not([data-scramjet])");
for (const script of scripts) {
const clone = document.createElement("script");
for (const attr of Array.from(script.attributes)) {
clone.setAttribute(attr.name, attr.value);
}
if (script.innerHTML !== "") {
clone.innerHTML = script.innerHTML;
}
script.insertAdjacentElement("afterend", clone);
script.remove();
}

View file

@ -1,25 +1,32 @@
// @ts-nocheck // @ts-nocheck
import { encodeUrl, decodeUrl } from "../shared"; import { encodeUrl, decodeUrl } from "./shared";
const loc = new URL(decodeUrl(location.href)); function createLocation() {
loc.assign = (url: string) => location.assign(encodeUrl(url)); const loc = new URL(decodeUrl(location.href));
loc.reload = () => location.reload(); loc.assign = (url: string) => location.assign(encodeUrl(url));
loc.replace = (url: string) => location.replace(encodeUrl(url)); loc.reload = () => location.reload();
loc.toString = () => loc.href; loc.replace = (url: string) => location.replace(encodeUrl(url));
loc.toString = () => loc.href;
export const locationProxy = new Proxy(window.location, {
get(target, prop) { return loc;
return loc[prop]; }
},
export const locationProxy = new Proxy(window.location, {
set(obj, prop, value) { get(target, prop) {
if (prop === "href") { const loc = createLocation();
location.href = encodeUrl(value);
} else { return loc[prop];
loc[prop] = value; },
}
set(obj, prop, value) {
return true; const loc = createLocation();
}
}) if (prop === "href") {
location.href = encodeUrl(value);
} else {
loc[prop] = value;
}
return true;
},
});

View file

@ -1,28 +1,34 @@
import { rewriteJs } from "../../shared"; import { rewriteJs } from "../shared";
const FunctionProxy = new Proxy(Function, { const FunctionProxy = new Proxy(Function, {
construct(target, argArray) { construct(target, argArray) {
if (argArray.length === 1) { if (argArray.length === 1) {
return Reflect.construct(target, rewriteJs(argArray[0])); return Reflect.construct(target, rewriteJs(argArray[0]));
} else { } else {
return Reflect.construct(target, 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, rewriteJs(argArray[0])); },
} else { apply(target, thisArg, argArray) {
return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), rewriteJs(argArray[argArray.length - 1])]) if (argArray.length === 1) {
} return Reflect.apply(target, undefined, [rewriteJs(argArray[0])]);
}, } else {
}); return Reflect.apply(target, undefined, [
...argArray.map((x, index) => index === argArray.length - 1),
delete window.Function; rewriteJs(argArray[argArray.length - 1]),
]);
window.Function = FunctionProxy; }
},
delete window.eval; });
// since the function proxy is already rewriting the js we can just reuse it for the eval proxy delete window.Function;
window.eval = (str: string) => window.Function(str); window.Function = FunctionProxy;
window.eval = new Proxy(window.eval, {
apply(target, thisArg, argArray) {
return Reflect.apply(target, thisArg, [rewriteJs(argArray[0])]);
},
});

View file

@ -1,35 +1,35 @@
// ts throws an error if you dont do window.fetch // ts throws an error if you dont do window.fetch
import { encodeUrl, rewriteHeaders } from "../../shared"; import { encodeUrl, rewriteHeaders } from "../shared";
window.fetch = new Proxy(window.fetch, { window.fetch = new Proxy(window.fetch, {
apply(target, thisArg, argArray) { apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]); argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray); return Reflect.apply(target, thisArg, argArray);
}, },
}); });
Headers = new Proxy(Headers, { Headers = new Proxy(Headers, {
construct(target, argArray, newTarget) { construct(target, argArray, newTarget) {
argArray[0] = rewriteHeaders(argArray[0]); argArray[0] = rewriteHeaders(argArray[0]);
return Reflect.construct(target, argArray, newTarget); return Reflect.construct(target, argArray, newTarget);
}, },
}) });
Request = new Proxy(Request, { Request = new Proxy(Request, {
construct(target, argArray, newTarget) { construct(target, argArray, newTarget) {
if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]); if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]);
return Reflect.construct(target, argArray, newTarget); return Reflect.construct(target, argArray, newTarget);
}, },
}); });
Response.redirect = new Proxy(Response.redirect, { Response.redirect = new Proxy(Response.redirect, {
apply(target, thisArg, argArray) { apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]); argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray); return Reflect.apply(target, thisArg, argArray);
}, },
}); });

View file

@ -1,15 +1,16 @@
import { BareClient } from "@mercuryworkshop/bare-mux" import { BareClient } from "../shared";
const client = new BareClient() const client = new BareClient();
WebSocket = new Proxy(WebSocket, {
construct(target, args) { WebSocket = new Proxy(WebSocket, {
return client.createWebSocket( construct(target, args) {
args[0], return client.createWebSocket(
args[1], args[0],
target, args[1],
{ target,
"User-Agent": navigator.userAgent {
}, "User-Agent": navigator.userAgent,
ArrayBuffer.prototype },
) ArrayBuffer.prototype
} );
}); },
});

View file

@ -1,20 +1,23 @@
import { encodeUrl, rewriteHeaders } from "../../shared"; import { encodeUrl, rewriteHeaders } from "../shared";
XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, { XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, {
apply(target, thisArg, argArray) { apply(target, thisArg, argArray) {
if (argArray[1]) argArray[1] = encodeUrl(argArray[1]); if (argArray[1]) argArray[1] = encodeUrl(argArray[1]);
return Reflect.apply(target, thisArg, argArray); return Reflect.apply(target, thisArg, argArray);
}, },
}); });
XMLHttpRequest.prototype.setRequestHeader = new Proxy(XMLHttpRequest.prototype.setRequestHeader, { XMLHttpRequest.prototype.setRequestHeader = new Proxy(
apply(target, thisArg, argArray) { XMLHttpRequest.prototype.setRequestHeader,
let headerObject = Object.fromEntries([argArray]); {
headerObject = rewriteHeaders(headerObject); apply(target, thisArg, argArray) {
let headerObject = Object.fromEntries([argArray]);
argArray = Object.entries(headerObject)[0]; headerObject = rewriteHeaders(headerObject);
return Reflect.apply(target, thisArg, argArray); argArray = Object.entries(headerObject)[0];
},
}); return Reflect.apply(target, thisArg, argArray);
},
}
);

View file

@ -2,14 +2,14 @@ import { locationProxy } from "./location";
import { windowProxy } from "./window"; import { windowProxy } from "./window";
function scope(identifier: any) { function scope(identifier: any) {
if (identifier instanceof Window) { if (identifier instanceof Window) {
return windowProxy; return windowProxy;
} else if (identifier instanceof Location) { } else if (identifier instanceof Location) {
return locationProxy; return locationProxy;
} }
return identifier; return identifier;
} }
// shorthand because this can get out of hand reall quickly // shorthand because this can get out of hand reall quickly
window.__s = scope; window.$s = scope;

12
src/client/shared.ts Normal file
View file

@ -0,0 +1,12 @@
export const {
util: { isScramjetFile, BareClient },
url: { encodeUrl, decodeUrl },
rewrite: {
rewriteCss,
rewriteHtml,
rewriteSrcset,
rewriteJs,
rewriteHeaders,
rewriteWorkers,
},
} = self.$scramjet.shared;

View file

@ -1,71 +1,70 @@
import IDBMapSync from "@webreflection/idb-map/sync"; import IDBMapSync from "@webreflection/idb-map/sync";
import { locationProxy } from "./location"; import { locationProxy } from "./location";
const store = new IDBMapSync(locationProxy.host, { const store = new IDBMapSync(locationProxy.host, {
prefix: "Storage", prefix: "Storage",
durability: "relaxed" durability: "relaxed",
}); });
await store.sync(); await store.sync();
function storageProxy(scope: Storage): Storage { function storageProxy(scope: Storage): Storage {
return new Proxy(scope, {
return new Proxy(scope, { get(target, prop) {
get(target, prop) { switch (prop) {
switch (prop) { case "getItem":
case "getItem": return (key: string) => {
return (key: string) => { return store.get(key);
return store.get(key); };
}
case "setItem":
case "setItem": return (key: string, value: string) => {
return (key: string, value: string) => { store.set(key, value);
store.set(key, value); store.sync();
store.sync(); };
}
case "removeItem":
case "removeItem": return (key: string) => {
return (key: string) => { store.delete(key);
store.delete(key); store.sync();
store.sync(); };
}
case "clear":
case "clear": return () => {
return () => { store.clear();
store.clear(); store.sync();
store.sync(); };
}
case "key":
case "key": return (index: number) => {
return (index: number) => { store.keys()[index];
store.keys()[index]; };
} case "length":
case "length": return store.size;
return store.size; default:
default: return store.get(prop);
return store.get(prop); }
} },
},
//@ts-ignore
//@ts-ignore set(target, prop, value) {
set(target, prop, value) { store.set(prop, value);
store.set(prop, value); store.sync();
store.sync(); },
},
defineProperty(target, property, attributes) {
defineProperty(target, property, attributes) { store.set(property as string, attributes.value);
store.set(property as string, attributes.value);
return true;
return true; },
}, });
}) }
}
const localStorageProxy = storageProxy(window.localStorage);
const localStorageProxy = storageProxy(window.localStorage); const sessionStorageProxy = storageProxy(window.sessionStorage);
const sessionStorageProxy = storageProxy(window.sessionStorage);
delete window.localStorage;
delete window.localStorage; delete window.sessionStorage;
delete window.sessionStorage;
window.localStorage = localStorageProxy;
window.localStorage = localStorageProxy; window.sessionStorage = sessionStorageProxy;
window.sessionStorage = sessionStorageProxy;

View file

@ -1,32 +1,40 @@
import { rewriteHtml, rewriteJs, encodeUrl } from "../shared"; // import { rewriteHtml, rewriteJs, encodeUrl } from "./shared";
// @ts-expect-error // trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, {
trustedTypes.createPolicy = new Proxy(trustedTypes.createPolicy, { // apply(target, thisArg, argArray) {
apply(target, thisArg, argArray) { // if (argArray[1].createHTML) {
if (argArray[1].createHTML) { // argArray[1].createHTML = new Proxy(argArray[1].createHTML, {
argArray[1].createHTML = new Proxy(argArray[1].createHTML, { // apply(target1, thisArg1, argArray1) {
apply(target1, thisArg1, argArray1) { // return rewriteHtml(target1(...argArray1));
return rewriteHtml(target1(...argArray1)); // },
}, // });
}); // }
} //
// if (argArray[1].createScript) {
if (argArray[1].createScript) { // argArray[1].createScript = new Proxy(argArray[1].createScript, {
argArray[1].createScript = new Proxy(argArray[1].createScript, { // apply(target1, thisArg1, argArray1) {
apply(target1, thisArg1, argArray1) { // return rewriteJs(target1(...argArray1));
return rewriteJs(target1(...argArray1)); // },
}, // });
}); // }
} //
// if (argArray[1].createScriptURL) {
if (argArray[1].createScriptURL) { // argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, {
argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, { // apply(target1, thisArg1, argArray1) {
apply(target1, thisArg1, argArray1) { // return encodeUrl(target1(...argArray1));
return encodeUrl(target1(...argArray1)); // },
}, // });
}) // }
} //
// return Reflect.apply(target, thisArg, argArray);
return Reflect.apply(target, thisArg, argArray); // },
}, // });
})
//@ts-nocheck
delete window.TrustedHTML;
delete window.TrustedScript;
delete window.TrustedScriptURL;
delete window.TrustedTypePolicy;
delete window.TrustedTypePolicyFactory;
delete window.trustedTypes;

13
src/client/url.ts Normal file
View file

@ -0,0 +1,13 @@
import { encodeUrl } from "../shared/rewriters/url";
export const URL = globalThis.URL;
if (globalThis.window) {
window.URL = new Proxy(URL, {
construct(target, argArray, newTarget) {
if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]);
if (typeof argArray[1] === "string") argArray[1] = encodeUrl(argArray[1]);
return Reflect.construct(target, argArray, newTarget);
},
});
}

View file

@ -1,23 +1,47 @@
import { locationProxy } from "./location"; import { locationProxy } from "./location";
export const windowProxy = new Proxy(window, { export const windowProxy = new Proxy(window, {
get(target, prop) { get(target, prop) {
const propIsString = typeof prop === "string"; const propIsString = typeof prop === "string";
if (propIsString && prop === "location") { if (propIsString && prop === "location") {
return locationProxy; return locationProxy;
} else if (propIsString && ["window", "top", "parent", "self", "globalThis"].includes(prop)) { } else if (
return windowProxy; propIsString &&
} ["window", "top", "parent", "self", "globalThis"].includes(prop)
) {
return windowProxy;
} else if (propIsString && prop === "$scramjet") {
return;
} else if (propIsString && prop === "addEventListener") {
console.log("addEventListener getteetetetetet");
return target[prop]; return new Proxy(window.addEventListener, {
}, apply(target1, thisArg, argArray) {
window.addEventListener(argArray[0], argArray[1]);
},
});
}
set(target, prop, newValue) { const value = Reflect.get(target, prop);
// ensures that no apis are overwritten
if (typeof prop === "string" && ["window", "top", "parent", "self", "globalThis", "location"].includes(prop)) {
return false;
}
return Reflect.set(target, prop, newValue); if (typeof value === "function") {
}, return value.bind(target);
}
return value;
},
set(target, prop, newValue) {
// ensures that no apis are overwritten
if (
typeof prop === "string" &&
["window", "top", "parent", "self", "globalThis", "location"].includes(
prop
)
) {
return false;
}
return Reflect.set(target, prop, newValue);
},
}); });

View file

@ -1,31 +1,33 @@
import { encodeUrl } from "../shared"; import { encodeUrl } from "./shared";
Worker = new Proxy(Worker, { Worker = new Proxy(Worker, {
construct(target, argArray) { construct(target, argArray) {
argArray[0] = encodeUrl(argArray[0]); argArray[0] = encodeUrl(argArray[0]);
// target is a reference to the object that you are proxying // target is a reference to the object that you are proxying
// Reflect.construct is just a wrapper for calling target // Reflect.construct is just a wrapper for calling target
// you could do new target(...argArray) and it would work the same effectively // you could do new target(...argArray) and it would work the same effectively
return Reflect.construct(target, argArray); return Reflect.construct(target, argArray);
} },
}) });
Worklet.prototype.addModule = new Proxy(Worklet.prototype.addModule, { Worklet.prototype.addModule = new Proxy(Worklet.prototype.addModule, {
apply(target, thisArg, argArray) { apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]) argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray); return Reflect.apply(target, thisArg, argArray);
}, },
}); });
window.importScripts = new Proxy(window.importScripts, { // broken
apply(target, thisArg, argArray) {
for (const i in argArray) { // window.importScripts = new Proxy(window.importScripts, {
argArray[i] = encodeUrl(argArray[i]); // apply(target, thisArg, argArray) {
} // for (const i in argArray) {
// argArray[i] = encodeUrl(argArray[i]);
return Reflect.apply(target, thisArg, argArray); // }
},
}); // return Reflect.apply(target, thisArg, argArray);
// },
// });

File diff suppressed because it is too large Load diff

View file

@ -1,83 +1,89 @@
import { enc, dec } from "./aes"; import { enc, dec } from "./aes";
// for some reason eslint was parsing the type inside of the function params as a variable // for some reason eslint was parsing the type inside of the function params as a variable
export interface Codec { export interface Codec {
// eslint-disable-next-line // eslint-disable-next-line
encode: (str: string | undefined) => string; encode: (str: string | undefined) => string;
// eslint-disable-next-line // eslint-disable-next-line
decode: (str: string | undefined) => string; decode: (str: string | undefined) => string;
} }
const xor = { const xor = {
encode: (str: string | undefined, key: number = 2) => { encode: (str: string | undefined, key: number = 2) => {
if (!str) return str; if (!str) return str;
return encodeURIComponent(str.split("").map((e, i) => i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e).join("")); return encodeURIComponent(
}, str
decode: (str: string | undefined, key: number = 2) => { .split("")
if (!str) return str; .map((e, i) =>
i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e
return decodeURIComponent(str).split("").map((e, i) => i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e).join(""); )
} .join("")
} );
},
const plain = { decode: (str: string | undefined, key: number = 2) => {
encode: (str: string | undefined) => { if (!str) return str;
if (!str) return str;
return decodeURIComponent(str)
return encodeURIComponent(str); .split("")
}, .map((e, i) => (i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e))
decode: (str: string | undefined) => { .join("");
if (!str) return str; },
};
return decodeURIComponent(str);
} const plain = {
} encode: (str: string | undefined) => {
if (!str) return str;
/*
const aes = { return encodeURIComponent(str);
encode: (str: string | undefined) => { },
if (!str) return str; decode: (str: string | undefined) => {
if (!str) return str;
return encodeURIComponent(enc(str, "dynamic").substring(10));
}, return decodeURIComponent(str);
decode: (str: string | undefined) => { },
if (!str) return str; };
return dec("U2FsdGVkX1" + decodeURIComponent(str), "dynamic"); /*
} const aes = {
} encode: (str: string | undefined) => {
*/ if (!str) return str;
const none = { return encodeURIComponent(enc(str, "dynamic").substring(10));
encode: (str: string | undefined) => str, },
decode: (str: string | undefined) => str, decode: (str: string | undefined) => {
} if (!str) return str;
const base64 = { return dec("U2FsdGVkX1" + decodeURIComponent(str), "dynamic");
encode: (str: string | undefined) => { }
if (!str) return str; }
*/
return decodeURIComponent(btoa(str));
}, const none = {
decode: (str: string | undefined) => { encode: (str: string | undefined) => str,
if (!str) return str; decode: (str: string | undefined) => str,
};
return atob(str);
} const base64 = {
} encode: (str: string | undefined) => {
if (!str) return str;
declare global {
interface Window { return decodeURIComponent(btoa(str));
__scramjet$codecs: { },
none: Codec; decode: (str: string | undefined) => {
plain: Codec; if (!str) return str;
base64: Codec;
xor: Codec; return atob(str);
} },
} };
}
if (!self.$scramjet) {
self.__scramjet$codecs = { //@ts-expect-error really dumb workaround
none, plain, base64, xor self.$scramjet = {};
} }
self.$scramjet.codecs = {
none,
plain,
base64,
xor,
};

View file

@ -1,25 +1,13 @@
import { Codec } from "./codecs"; if (!self.$scramjet) {
//@ts-expect-error really dumb workaround
declare global { self.$scramjet = {};
interface Window { }
__scramjet$config: { self.$scramjet.config = {
prefix: string; prefix: "/scramjet/",
codec: Codec codec: self.$scramjet.codecs.plain,
config: string; config: "/scram/scramjet.config.js",
shared: string; shared: "/scram/scramjet.shared.js",
worker: string; worker: "/scram/scramjet.worker.js",
client: string; client: "/scram/scramjet.client.js",
codecs: string; codecs: "/scram/scramjet.codecs.js",
} };
}
}
self.__scramjet$config = {
prefix: "/scramjet/",
codec: self.__scramjet$codecs.plain,
config: "/scram/scramjet.config.js",
shared: "/scram/scramjet.shared.js",
worker: "/scram/scramjet.worker.js",
client: "/scram/scramjet.client.js",
codecs: "/scram/scramjet.codecs.js"
}

View file

@ -1,16 +1,33 @@
export { encodeUrl, decodeUrl } from "./rewriters/url"; import { encodeUrl, decodeUrl } from "./rewriters/url";
export { rewriteCss } from "./rewriters/css"; import { rewriteCss } from "./rewriters/css";
export { rewriteHtml, rewriteSrcset } from "./rewriters/html"; import { rewriteHtml, rewriteSrcset } from "./rewriters/html";
export { rewriteJs } from "./rewriters/js"; import { rewriteJs } from "./rewriters/js";
export { rewriteHeaders } from "./rewriters/headers"; import { rewriteHeaders } from "./rewriters/headers";
export { rewriteWorkers } from "./rewriters/worker" import { rewriteWorkers } from "./rewriters/worker";
export { BareClient } from "@mercuryworkshop/bare-mux" import { isScramjetFile } from "./rewriters/html";
import { BareClient } from "@mercuryworkshop/bare-mux";
import { parseDomain } from "parse-domain";
export function isScramjetFile(src: string) { if (!self.$scramjet) {
let bool = false; //@ts-expect-error really dumb workaround
["codecs", "client", "shared", "worker", "config"].forEach((file) => { self.$scramjet = {};
if (src === self.__scramjet$config[file]) bool = true; }
}); self.$scramjet.shared = {
util: {
return bool; isScramjetFile,
} parseDomain,
BareClient,
},
url: {
encodeUrl,
decodeUrl,
},
rewrite: {
rewriteCss,
rewriteHtml,
rewriteSrcset,
rewriteJs,
rewriteHeaders,
rewriteWorkers,
},
};

View file

@ -4,32 +4,31 @@
import { encodeUrl } from "./url"; import { encodeUrl } from "./url";
export function rewriteCss(css: string, origin?: URL) { export function rewriteCss(css: string, origin?: URL) {
const regex = const regex =
/(@import\s+(?!url\())?\s*url\(\s*(['"]?)([^'")]+)\2\s*\)|@import\s+(['"])([^'"]+)\4/g /(@import\s+(?!url\())?\s*url\(\s*(['"]?)([^'")]+)\2\s*\)|@import\s+(['"])([^'"]+)\4/g;
return css.replace( return css.replace(
regex, regex,
( (
match, match,
importStatement, importStatement,
urlQuote, urlQuote,
urlContent, urlContent,
importQuote, importQuote,
importContent importContent
) => { ) => {
const url = urlContent || importContent const url = urlContent || importContent;
const encodedUrl = encodeUrl(url.trim(), origin) const encodedUrl = encodeUrl(url.trim(), origin);
if (importStatement) { if (importStatement) {
return `@import url(${urlQuote}${encodedUrl}${urlQuote})` return `@import url(${urlQuote}${encodedUrl}${urlQuote})`;
} }
if (importQuote) { if (importQuote) {
return `@import ${importQuote}${encodedUrl}${importQuote}` return `@import ${importQuote}${encodedUrl}${importQuote}`;
} }
return `url(${urlQuote}${encodedUrl}${urlQuote})`
}
)
return `url(${urlQuote}${encodedUrl}${urlQuote})`;
}
);
} }

View file

@ -1,52 +1,50 @@
import { encodeUrl } from "./url"; import { encodeUrl } from "./url";
import { BareHeaders } from "@mercuryworkshop/bare-mux"; import { BareHeaders } from "@mercuryworkshop/bare-mux";
const cspHeaders = [ const cspHeaders = [
"cross-origin-embedder-policy", "cross-origin-embedder-policy",
"cross-origin-opener-policy", "cross-origin-opener-policy",
"cross-origin-resource-policy", "cross-origin-resource-policy",
"content-security-policy", "content-security-policy",
"content-security-policy-report-only", "content-security-policy-report-only",
"expect-ct", "expect-ct",
"feature-policy", "feature-policy",
"origin-isolation", "origin-isolation",
"strict-transport-security", "strict-transport-security",
"upgrade-insecure-requests", "upgrade-insecure-requests",
"x-content-type-options", "x-content-type-options",
"x-download-options", "x-download-options",
"x-frame-options", "x-frame-options",
"x-permitted-cross-domain-policies", "x-permitted-cross-domain-policies",
"x-powered-by", "x-powered-by",
"x-xss-protection", "x-xss-protection",
// This needs to be emulated, but for right now it isn't that important of a feature to be worried about // This needs to be emulated, but for right now it isn't that important of a feature to be worried about
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
"clear-site-data" "clear-site-data",
]; ];
const urlHeaders = [ const urlHeaders = ["location", "content-location", "referer"];
"location",
"content-location",
"referer"
];
export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) { export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) {
const headers = {}; const headers = {};
for (const key in rawHeaders) { for (const key in rawHeaders) {
headers[key.toLowerCase()] = rawHeaders[key]; headers[key.toLowerCase()] = rawHeaders[key];
} }
cspHeaders.forEach((header) => { cspHeaders.forEach((header) => {
delete headers[header]; delete headers[header];
}); });
urlHeaders.forEach((header) => { urlHeaders.forEach((header) => {
if (headers[header]) if (headers[header])
headers[header] = encodeUrl(headers[header] as string, origin); headers[header] = encodeUrl(headers[header] as string, origin);
}); });
if (headers["link"]) { if (headers["link"]) {
headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) => encodeUrl(match, origin)); headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) =>
} encodeUrl(match, origin)
);
}
return headers; return headers;
} }

View file

@ -4,95 +4,124 @@ import { hasAttrib } from "domutils";
import render from "dom-serializer"; 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 { isScramjetFile } from "..";
export function isScramjetFile(src: string) {
let bool = false;
["codecs", "client", "shared", "worker", "config"].forEach((file) => {
if (src === self.$scramjet.config[file]) bool = true;
});
return bool;
}
export function rewriteHtml(html: string, origin?: URL) { export function rewriteHtml(html: string, 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, 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, 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)) {
node.attribs[`data-${cspAttr}`] = node.attribs[cspAttr]; node.attribs[`data-${cspAttr}`] = node.attribs[cspAttr];
delete node.attribs[cspAttr]; delete node.attribs[cspAttr];
} }
} }
/* url attributes */ /* url attributes */
for (const urlAttr of ["src", "href", "data", "action", "formaction"]) { for (const urlAttr of ["src", "href", "action", "formaction"]) {
if (hasAttrib(node, urlAttr) && !isScramjetFile(node.attribs[urlAttr])) { if (hasAttrib(node, urlAttr) && !isScramjetFile(node.attribs[urlAttr])) {
const value = node.attribs[urlAttr]; const value = node.attribs[urlAttr];
node.attribs[`data-${urlAttr}`] = value; node.attribs[`data-${urlAttr}`] = value;
node.attribs[urlAttr] = encodeUrl(value, origin); node.attribs[urlAttr] = encodeUrl(value, origin);
} }
} }
/* other */
for (const srcsetAttr of ["srcset", "imagesrcset"]) {
if (hasAttrib(node, srcsetAttr)) {
const value = node.attribs[srcsetAttr];
node.attribs[`data-${srcsetAttr}`] = value;
node.attribs[srcsetAttr] = rewriteSrcset(value, origin);
}
}
if (hasAttrib(node, "srcdoc")) node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin); /* other */
if (hasAttrib(node, "style")) node.attribs.style = rewriteCss(node.attribs.style, origin); for (const srcsetAttr of ["srcset", "imagesrcset"]) {
if (hasAttrib(node, srcsetAttr)) {
const value = node.attribs[srcsetAttr];
node.attribs[`data-${srcsetAttr}`] = value;
node.attribs[srcsetAttr] = rewriteSrcset(value, origin);
}
}
if (node.name === "style" && node.children[0] !== undefined) node.children[0].data = rewriteCss(node.children[0].data, origin); if (hasAttrib(node, "srcdoc"))
// 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); node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin);
if (node.name === "meta" && hasAttrib(node, "http-equiv")) { if (hasAttrib(node, "style"))
if (node.attribs["http-equiv"] === "content-security-policy") { node.attribs.style = rewriteCss(node.attribs.style, origin);
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") { if (node.name === "style" && node.children[0] !== undefined)
const scramjetScripts = []; node.children[0].data = rewriteCss(node.children[0].data, origin);
["codecs", "config", "shared", "client"].forEach((script) => { if (
scramjetScripts.push(new Element("script", { node.name === "script" &&
src: self.__scramjet$config[script], /(application|text)\/javascript|importmap|undefined/.test(
type: "module", node.attribs.type
"data-scramjet": "" ) &&
})); node.children[0] !== undefined
}); ) {
let js = node.children[0].data
const htmlcomment = /<!--[\s\S]*?-->/g;
js = js.replace(htmlcomment, "");
node.children[0].data = rewriteJs(js, 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=");
}
}
node.children.unshift(...scramjetScripts); if (node.name === "head") {
} const scramjetScripts = [];
["codecs", "config", "shared", "client"].forEach((script) => {
scramjetScripts.push(
new Element("script", {
src: self.$scramjet.config[script],
"data-scramjet": "",
})
);
});
if (node.childNodes) { node.children.unshift(...scramjetScripts);
for (const childNode in node.childNodes) { }
node.childNodes[childNode] = traverseParsedHtml(node.childNodes[childNode], origin);
}
}
return node; if (node.childNodes) {
for (const childNode in node.childNodes) {
node.childNodes[childNode] = traverseParsedHtml(
node.childNodes[childNode],
origin
);
}
}
return node;
} }
export function rewriteSrcset(srcset: string, origin?: URL) { export function rewriteSrcset(srcset: string, origin?: URL) {
const urls = srcset.split(/ [0-9]+x,? ?/g); const urls = srcset.split(/ [0-9]+x,? ?/g);
if (!urls) return ""; if (!urls) return "";
const sufixes = srcset.match(/ [0-9]+x,? ?/g); const sufixes = srcset.match(/ [0-9]+x,? ?/g);
if (!sufixes) return ""; if (!sufixes) return "";
const rewrittenUrls = urls.map((url, i) => { const rewrittenUrls = urls.map((url, i) => {
if (url && sufixes[i]) { if (url && sufixes[i]) {
return encodeUrl(url, origin) + sufixes[i]; return encodeUrl(url, origin) + sufixes[i];
} }
}); });
return rewrittenUrls.join(""); return rewrittenUrls.join("");
} }

View file

@ -16,64 +16,92 @@ import * as ESTree from "estree";
// top // top
// parent // parent
export function rewriteJs(js: string, origin?: URL) { export function rewriteJs(js: string, origin?: URL) {
try { try {
const ast = parseModule(js, { const ast = parseModule(js, {
module: true, module: true,
webcompat: true webcompat: true,
}); });
const identifierList = [
"window",
"self",
"globalThis",
"this",
"parent",
"top",
"this",
"location"
]
const customTraveler = makeTraveler({
ImportDeclaration: (node: ESTree.ImportDeclaration) => {
node.source.value = encodeUrl(node.source.value as string, origin);
},
ImportExpression: (node: ESTree.ImportExpression) => {
if (node.source.type === "Literal") {
node.source.value = encodeUrl(node.source.value as string, origin);
} else if (node.source.type === "Identifier") {
// this is for things that import something like
// const moduleName = "name";
// await import(moduleName);
node.source.name = `__wrapImport(${node.source.name})`;
}
},
ExportAllDeclaration: (node: ESTree.ExportAllDeclaration) => {
node.source.value = encodeUrl(node.source.value as string, origin);
},
ExportNamedDeclaration: (node: ESTree.ExportNamedDeclaration) => {
// strings are Literals in ESTree syntax but these will always be strings
if (node.source) node.source.value = encodeUrl(node.source.value as string, origin);
},
// js rweriting notrdone const identifierList = [
MemberExpression: (node: ESTree.MemberExpression) => { "window",
if (node.object.type === "Identifier" && identifierList.includes(node.object.name)) { "self",
node.object.name = `__s(${node.object.name})`; "globalThis",
} "this",
} "parent",
}); "top",
"location",
customTraveler.go(ast); ];
return generate(ast);
} catch {
console.log(js);
return js; const customTraveler = makeTraveler({
} ImportDeclaration: (node: ESTree.ImportDeclaration) => {
node.source.value = encodeUrl(node.source.value as string, origin);
},
ImportExpression: (node: ESTree.ImportExpression) => {
if (node.source.type === "Literal") {
node.source.value = encodeUrl(node.source.value as string, origin);
} else if (node.source.type === "Identifier") {
// this is for things that import something like
// const moduleName = "name";
// await import(moduleName);
node.source.name = `__wrapImport(${node.source.name})`;
}
},
ExportAllDeclaration: (node: ESTree.ExportAllDeclaration) => {
node.source.value = encodeUrl(node.source.value as string, origin);
},
ExportNamedDeclaration: (node: ESTree.ExportNamedDeclaration) => {
// strings are Literals in ESTree syntax but these will always be strings
if (node.source)
node.source.value = encodeUrl(node.source.value as string, origin);
},
MemberExpression: (node: ESTree.MemberExpression) => {
if (
node.object.type === "Identifier" &&
identifierList.includes(node.object.name)
) {
node.object.name = `globalThis.$s(${node.object.name})`;
}
},
AssignmentExpression: (node: ESTree.AssignmentExpression) => {
if (
node.left.type === "Identifier" &&
identifierList.includes(node.left.name)
) {
node.left.name = `globalThis.$s(${node.left.name})`;
}
if (
node.right.type === "Identifier" &&
identifierList.includes(node.right.name)
) {
node.right.name = `globalThis.$s(${node.right.name})`;
}
},
VariableDeclarator: (node: ESTree.VariableDeclarator) => {
if (
node.init &&
node.init.type === "Identifier" &&
identifierList.includes(node.init.name)
) {
node.init.name = `globalThis.$s(${node.init.name})`;
}
},
});
customTraveler.go(ast);
return generate(ast);
} catch (e) {
console.error(e);
console.log(js);
return js;
}
} }

View file

@ -1,37 +1,61 @@
import { URL } from "../../client/url";
import { rewriteJs } from "./js"; import { rewriteJs } from "./js";
function canParseUrl(url: string, origin?: URL) { function canParseUrl(url: string, origin?: URL) {
try { try {
new URL(url, origin); new URL(url, origin);
return true; return true;
} catch { } catch {
return false; return false;
} }
} }
// something is broken with this but i didn't debug it // something is broken with this but i didn't debug it
export function encodeUrl(url: string, origin?: URL) { export function encodeUrl(url: string | URL, origin?: URL) {
if (!origin) { if (url instanceof URL) {
origin = new URL(self.__scramjet$config.codec.decode(location.href.slice((location.origin + self.__scramjet$config.prefix).length))); return url.toString();
} }
if (url.startsWith("javascript:")) { if (!origin) {
return "javascript:" + rewriteJs(url.slice("javascript:".length)); origin = new URL(
} else if (/^(#|mailto|about|data)/.test(url)) { self.$scramjet.config.codec.decode(
return url; location.href.slice(
} else if (canParseUrl(url, origin)) { (location.origin + self.$scramjet.config.prefix).length
return location.origin + self.__scramjet$config.prefix + self.__scramjet$config.codec.encode(new URL(url, origin).href); )
} )
);
}
// is this the correct behavior?
if (!url) url = origin.href;
if (url.startsWith("javascript:")) {
return "javascript:" + rewriteJs(url.slice("javascript:".length));
} else if (/^(#|mailto|about|data)/.test(url)) {
return url;
} else if (canParseUrl(url, origin)) {
return (
location.origin +
self.$scramjet.config.prefix +
self.$scramjet.config.codec.encode(new URL(url, origin).href)
);
}
} }
// something is also broken with this but i didn't debug it // something is also broken with this but i didn't debug it
export function decodeUrl(url: string) { export function decodeUrl(url: string | URL) {
if (/^(#|about|data|mailto|javascript)/.test(url)) { if (url instanceof URL) {
return url; return url.toString();
} else if (canParseUrl(url)) { }
return self.__scramjet$config.codec.decode(url.slice((location.origin + self.__scramjet$config.prefix).length))
} else { if (/^(#|about|data|mailto|javascript)/.test(url)) {
return 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;
}
}

View file

@ -1,11 +1,12 @@
import { rewriteJs } from "./js"; import { rewriteJs } from "./js";
export function rewriteWorkers(js: string, origin?: URL) { export function rewriteWorkers(js: string, origin?: URL) {
let str = new String().toString() let str = new String().toString()[
//@ts-expect-error //@ts-expect-error
["codecs", "config", "shared", "client"].forEach((script) => { ("codecs", "config", "shared", "client")
str += `import "${self.__scramjet$config[script]}"\n` ].forEach((script) => {
}) str += `import "${self.$scramjet.config[script]}"\n`;
str += rewriteJs(js, origin); });
str += rewriteJs(js, origin);
return str; return str;
} }

51
src/types.d.ts vendored Normal file
View file

@ -0,0 +1,51 @@
import { encodeUrl, decodeUrl } from "./shared/rewriters/url";
import { rewriteCss } from "./shared/rewriters/css";
import { rewriteHtml, rewriteSrcset } from "./shared/rewriters/html";
import { rewriteJs } from "./shared/rewriters/js";
import { rewriteHeaders } from "./shared/rewriters/headers";
import { rewriteWorkers } from "./shared/rewriters/worker";
import { isScramjetFile } from "./shared/rewriters/html";
import type { Codec } from "./codecs";
import { BareClient } from "@mercuryworkshop/bare-mux";
import { parseDomain } from "parse-domain";
declare global {
interface Window {
$scramjet: {
shared: {
url: {
encodeUrl: typeof encodeUrl;
decodeUrl: typeof decodeUrl;
};
rewrite: {
rewriteCss: typeof rewriteCss;
rewriteHtml: typeof rewriteHtml;
rewriteSrcset: typeof rewriteSrcset;
rewriteJs: typeof rewriteJs;
rewriteHeaders: typeof rewriteHeaders;
rewriteWorkers: typeof rewriteWorkers;
};
util: {
BareClient: typeof BareClient;
isScramjetFile: typeof isScramjetFile;
parseDomain: typeof parseDomain;
};
};
config: {
prefix: string;
codec: Codec;
config: string;
shared: string;
worker: string;
client: string;
codecs: string;
};
codecs: {
none: Codec;
plain: Codec;
base64: Codec;
xor: Codec;
};
};
}
}

View file

@ -1,199 +1,265 @@
import { BareClient } from "@mercuryworkshop/bare-mux"; import { BareResponseFetch } from "@mercuryworkshop/bare-mux";
import { BareResponseFetch } from "@mercuryworkshop/bare-mux"; import IDBMap from "@webreflection/idb-map";
import { encodeUrl, decodeUrl, rewriteCss, rewriteHeaders, rewriteHtml, rewriteJs, rewriteWorkers } from "../shared"; import { ParseResultType } from "parse-domain";
import { parse } from "path";
declare global {
interface Window { declare global {
ScramjetServiceWorker; interface Window {
} ScramjetServiceWorker;
} }
}
export default class ScramjetServiceWorker {
client: typeof BareClient.prototype; self.ScramjetServiceWorker = class ScramjetServiceWorker {
config: typeof self.__scramjet$config; client: typeof self.$scramjet.shared.util.BareClient.prototype;
constructor(config = self.__scramjet$config) { config: typeof self.$scramjet.config;
this.client = new BareClient();
if (!config.prefix) config.prefix = "/scramjet/"; constructor(config = self.$scramjet.config) {
this.config = config; this.client = new self.$scramjet.shared.util.BareClient();
} if (!config.prefix) config.prefix = "/scramjet/";
this.config = config;
route({ request }: FetchEvent) { }
if (request.url.startsWith(location.origin + this.config.prefix)) return true;
else return false; route({ request }: FetchEvent) {
} if (request.url.startsWith(location.origin + this.config.prefix))
return true;
async fetch({ request }: FetchEvent) { else return false;
const urlParam = new URLSearchParams(new URL(request.url).search); }
if (urlParam.has("url")) { async fetch({ request }: FetchEvent) {
return Response.redirect(encodeUrl(urlParam.get("url"), new URL(urlParam.get("url")))) const urlParam = new URLSearchParams(new URL(request.url).search);
} const { encodeUrl, decodeUrl } = self.$scramjet.shared.url;
const {
try { rewriteHeaders,
const url = new URL(decodeUrl(request.url)); rewriteHtml,
rewriteJs,
const response: BareResponseFetch = await this.client.fetch(url, { rewriteCss,
method: request.method, rewriteWorkers,
body: request.body, } = self.$scramjet.shared.rewrite;
headers: request.headers, const { parseDomain } = self.$scramjet.shared.util;
credentials: "omit",
mode: request.mode === "cors" ? request.mode : "same-origin", if (urlParam.has("url")) {
cache: request.cache, return Response.redirect(
redirect: request.redirect, encodeUrl(urlParam.get("url"), new URL(urlParam.get("url")))
}); );
}
let responseBody;
const responseHeaders = rewriteHeaders(response.rawHeaders, url); try {
if (response.body) { const url = new URL(decodeUrl(request.url));
switch (request.destination) {
case "iframe": const cookieStore = new IDBMap(url.host, {
case "document": durability: "relaxed",
if (responseHeaders["content-type"].startsWith("text/html")) { prefix: "Cookies",
responseBody = rewriteHtml(await response.text(), url); });
} else {
responseBody = response.body; const response: BareResponseFetch = await this.client.fetch(url, {
} method: request.method,
break; body: request.body,
case "script": headers: request.headers,
responseBody = rewriteJs(await response.text(), url); credentials: "omit",
break; mode: request.mode === "cors" ? request.mode : "same-origin",
case "style": cache: request.cache,
responseBody = rewriteCss(await response.text(), url); redirect: request.redirect,
break; //@ts-ignore why the fuck is this not typed mircosoft
case "sharedworker": duplex: "half",
case "worker": });
responseBody = rewriteWorkers(await response.text(), url);
break; let responseBody;
default: const responseHeaders = rewriteHeaders(response.rawHeaders, url);
responseBody = response.body;
break; for (const cookie of (responseHeaders["set-cookie"] || []) as string[]) {
} let cookieParsed = cookie.split(";").map((x) => x.trim().split("="));
}
// downloads let [key, value] = cookieParsed.shift();
if (["document", "iframe"].includes(request.destination)) { value = value.replace('"', "");
const header = responseHeaders["content-disposition"];
const hostArg = cookieParsed.find((x) => x[0] === "Domain");
// validate header and test for filename cookieParsed = cookieParsed.filter((x) => x[0] !== "Domain");
if (!/\s*?((inline|attachment);\s*?)filename=/i.test(header)) { let host = hostArg ? hostArg[1] : undefined;
// if filename= wasn"t specified then maybe the remote specified to download this as an attachment?
// if it"s invalid then we can still possibly test for the attachment/inline type if (url.protocol === "http" && cookieParsed.includes(["Secure"]))
const type = /^\s*?attachment/i.test(header) continue;
? "attachment" if (
: "inline"; cookieParsed.includes(["SameSite", "None"]) &&
!cookieParsed.includes(["Secure"])
// set the filename )
const [filename] = new URL(response.finalURL).pathname continue;
.split("/")
.slice(-1); if (host && host !== url.host) {
if (host.startsWith(".")) host = host.slice(1);
responseHeaders[ const urlDomain = parseDomain(url.hostname);
"content-disposition"
] = `${type}; filename=${JSON.stringify(filename)}`; if (urlDomain.type === ParseResultType.Listed) {
} const { subDomains: _, domain, topLevelDomains } = urlDomain;
} if (!host.endsWith([domain, ...topLevelDomains].join(".")))
if (responseHeaders["accept"] === "text/event-stream") { continue;
responseHeaders["content-type"] = "text/event-stream"; } else {
} continue;
if (crossOriginIsolated) { }
responseHeaders["Cross-Origin-Embedder-Policy"] = "require-corp";
} const realCookieStore = new IDBMap(host, {
durability: "relaxed",
return new Response(responseBody, { prefix: "Cookies",
headers: responseHeaders as HeadersInit, });
status: response.status, realCookieStore.set(key, {
statusText: response.statusText value: value,
}) args: cookieParsed,
} catch (err) { subdomain: true,
if (!["document", "iframe"].includes(request.destination)) });
return new Response(undefined, { status: 500 }); } else {
cookieStore.set(key, {
console.error(err); value: value,
args: cookieParsed,
return renderError(err, decodeUrl(request.url)); subdomain: false,
} });
} }
} }
for (let header in responseHeaders) {
function errorTemplate( // flatten everything past here
trace: string, if (responseHeaders[header] instanceof Array)
fetchedURL: string, responseHeaders[header] = responseHeaders[header][0];
) { }
// turn script into a data URI so we don"t have to escape any HTML values
const script = ` if (response.body) {
errorTrace.value = ${JSON.stringify(trace)}; switch (request.destination) {
fetchedURL.textContent = ${JSON.stringify(fetchedURL)}; case "iframe":
for (const node of document.querySelectorAll("#hostname")) node.textContent = ${JSON.stringify( case "document":
location.hostname if (
)}; responseHeaders["content-type"]
reload.addEventListener("click", () => location.reload()); ?.toString()
version.textContent = "0.0.1"; ?.startsWith("text/html")
` ) {
responseBody = rewriteHtml(await response.text(), url);
return ( } else {
`<!DOCTYPE html> responseBody = response.body;
<html> }
<head> break;
<meta charset="utf-8" /> case "script":
<title>Error</title> responseBody = rewriteJs(await response.text(), url);
<style> break;
* { background-color: white } case "style":
</style> responseBody = rewriteCss(await response.text(), url);
</head> break;
<body> case "sharedworker":
<h1 id="errorTitle">Error processing your request</h1> case "worker":
<hr /> responseBody = rewriteWorkers(await response.text(), url);
<p>Failed to load <b id="fetchedURL"></b></p> break;
<p id="errorMessage">Internal Server Error</p> default:
<textarea id="errorTrace" cols="40" rows="10" readonly></textarea> responseBody = response.body;
<p>Try:</p> break;
<ul> }
<li>Checking your internet connection</li> }
<li>Verifying you entered the correct address</li> // downloads
<li>Clearing the site data</li> if (["document", "iframe"].includes(request.destination)) {
<li>Contacting <b id="hostname"></b>"s administrator</li> const header = responseHeaders["content-disposition"];
<li>Verify the server isn"t censored</li>
</ul> // validate header and test for filename
<p>If you"re the administrator of <b id="hostname"></b>, try:</p> if (!/\s*?((inline|attachment);\s*?)filename=/i.test(header)) {
<ul> // if filename= wasn"t specified then maybe the remote specified to download this as an attachment?
<li>Restarting your server</li> // if it"s invalid then we can still possibly test for the attachment/inline type
<li>Updating Scramjet</li> const type = /^\s*?attachment/i.test(header)
<li>Troubleshooting the error on the <a href="https://github.com/MercuryWorkshop/scramjet" target="_blank">GitHub repository</a></li> ? "attachment"
</ul> : "inline";
<button id="reload">Reload</button>
<hr /> // set the filename
<p><i>Scramjet v<span id="version"></span></i></p> const [filename] = new URL(response.finalURL).pathname
<script src="${ .split("/")
"data:application/javascript," + encodeURIComponent(script) .slice(-1);
}"></script>
</body> responseHeaders["content-disposition"] =
</html> `${type}; filename=${JSON.stringify(filename)}`;
` }
); }
} if (responseHeaders["accept"] === "text/event-stream") {
responseHeaders["content-type"] = "text/event-stream";
/** }
* if (crossOriginIsolated) {
* @param {unknown} err responseHeaders["Cross-Origin-Embedder-Policy"] = "require-corp";
* @param {string} fetchedURL }
*/
function renderError(err, fetchedURL) { return new Response(responseBody, {
const headers = { headers: responseHeaders as HeadersInit,
"content-type": "text/html", status: response.status,
}; statusText: response.statusText,
if (crossOriginIsolated) { });
headers["Cross-Origin-Embedd'er-Policy"] = "require-corp"; } catch (err) {
} if (!["document", "iframe"].includes(request.destination))
return new Response(undefined, { status: 500 });
return new Response(
errorTemplate( console.error(err);
String(err),
fetchedURL return renderError(err, decodeUrl(request.url));
), }
{ }
status: 500, };
headers: headers
} function errorTemplate(trace: string, fetchedURL: string) {
); // turn script into a data URI so we don"t have to escape any HTML values
} const script = `
errorTrace.value = ${JSON.stringify(trace)};
fetchedURL.textContent = ${JSON.stringify(fetchedURL)};
for (const node of document.querySelectorAll("#hostname")) node.textContent = ${JSON.stringify(
location.hostname
)};
reload.addEventListener("click", () => location.reload());
version.textContent = "0.0.1";
`;
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Error</title>
<style>
* { background-color: white }
</style>
</head>
<body>
<h1 id="errorTitle">Error processing your request</h1>
<hr />
<p>Failed to load <b id="fetchedURL"></b></p>
<p id="errorMessage">Internal Server Error</p>
<textarea id="errorTrace" cols="40" rows="10" readonly></textarea>
<p>Try:</p>
<ul>
<li>Checking your internet connection</li>
<li>Verifying you entered the correct address</li>
<li>Clearing the site data</li>
<li>Contacting <b id="hostname"></b>"s administrator</li>
<li>Verify the server isn"t censored</li>
</ul>
<p>If you"re the administrator of <b id="hostname"></b>, try:</p>
<ul>
<li>Restarting your server</li>
<li>Updating Scramjet</li>
<li>Troubleshooting the error on the <a href="https://github.com/MercuryWorkshop/scramjet" target="_blank">GitHub repository</a></li>
</ul>
<button id="reload">Reload</button>
<hr />
<p><i>Scramjet v<span id="version"></span></i></p>
<script src="${
"data:application/javascript," + encodeURIComponent(script)
}"></script>
</body>
</html>
`;
}
/**
*
* @param {unknown} err
* @param {string} fetchedURL
*/
function renderError(err, fetchedURL) {
const headers = {
"content-type": "text/html",
};
if (crossOriginIsolated) {
headers["Cross-Origin-Embedder-Policy"] = "require-corp";
}
return new Response(errorTemplate(String(err), fetchedURL), {
status: 500,
headers: headers,
});
}

View file

@ -1,29 +1,39 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
<link rel="prefetch" href="/scram/scramjet.worker.js" type="module"> <link rel="prefetch" href="/scram/scramjet.worker.js" />
<link rel="prefetch" href="/scram/scramjet.shared.js" type="module"> <link rel="prefetch" href="/scram/scramjet.shared.js" />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&amp;family=Inter+Tight:ital,wght@0,100..900;1,100..900&amp;family=Inter:wght@100..900&amp;display=swap&amp;" rel="stylesheet"> <link
<style> href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&amp;family=Inter+Tight:ital,wght@0,100..900;1,100..900&amp;family=Inter:wght@100..900&amp;display=swap&amp;"
body, html, #app { rel="stylesheet"
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; />
width:100vw; <style>
height:100vh; body,
margin: 0; html,
padding: 0; #app {
background-color:#121212; font-family:
overflow: hidden; "Inter",
} system-ui,
</style> -apple-system,
</head> BlinkMacSystemFont,
<body> sans-serif;
<script src="https://unpkg.com/dreamland"></script> width: 100vw;
<script src="/baremux/index.js" defer></script> height: 100vh;
<script src="/scram/scramjet.codecs.js" type="module"></script> margin: 0;
<script src="/scram/scramjet.config.js" type="module"></script> padding: 0;
<script src="ui.js" defer></script> background-color: #121212;
</body> overflow: hidden;
</html> }
</style>
</head>
<body>
<script src="https://unpkg.com/dreamland"></script>
<script src="/baremux/index.js" defer></script>
<script src="/scram/scramjet.codecs.js" defer></script>
<script src="/scram/scramjet.config.js" defer></script>
<script src="ui.js" defer></script>
</body>
</html>

View file

@ -1,17 +1,20 @@
import ScramjetServiceWorker from "/scram/scramjet.worker.js"; importScripts(
import "/scram/scramjet.codecs.js"; "/scram/scramjet.codecs.js",
import "/scram/scramjet.config.js"; "/scram/scramjet.config.js",
"/scram/scramjet.shared.js",
const scramjet = new ScramjetServiceWorker(); "/scram/scramjet.worker.js"
);
async function handleRequest(event) {
if (scramjet.route(event)) { const scramjet = new ScramjetServiceWorker();
return scramjet.fetch(event);
} async function handleRequest(event) {
if (scramjet.route(event)) {
return fetch(event.request) return scramjet.fetch(event);
} }
self.addEventListener("fetch", (event) => { return fetch(event.request);
event.respondWith(handleRequest(event)); }
});
self.addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event));
});

View file

@ -1,100 +1,114 @@
navigator.serviceWorker.register("./sw.js", { navigator.serviceWorker
scope: __scramjet$config.prefix, .register("./sw.js", {
type: "module" scope: $scramjet.config.prefix,
}) })
const connection = new BareMux.BareMuxConnection("/baremux/worker.js") .then((reg) => {
const flex = css`display: flex;`; reg.update();
const col = css`flex-direction: column;`; });
const store = $store({ const connection = new BareMux.BareMuxConnection("/baremux/worker.js");
url: "https://google.com", const flex = css`
wispurl: "wss://wisp.mercurywork.shop/", display: flex;
bareurl: (location.protocol === "https:" ? "https" : "http") + "://" + location.host + "/bare/", `;
}, { ident: "settings", backing: "localstorage", autosave: "auto" }); const col = css`
connection.setTransport("/baremod/index.mjs", [store.bareurl]) flex-direction: column;
function App() { `;
this.urlencoded = ""; const store = $store(
this.css = ` {
width: 100%; url: "https://google.com",
height: 100%; wispurl: "wss://wisp.mercurywork.shop/",
color: #e0def4; bareurl:
display: flex; (location.protocol === "https:" ? "https" : "http") +
align-items: center; "://" +
justify-content: center; location.host +
flex-direction: column; "/bare/",
input, },
button { { ident: "settings", backing: "localstorage", autosave: "auto" }
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, );
sans-serif; connection.setTransport("/baremod/index.mjs", [store.bareurl]);
} function App() {
h1 { this.urlencoded = "";
font-family: "Inter Tight", "Inter", system-ui, -apple-system, BlinkMacSystemFont, this.css = `
sans-serif; width: 100%;
margin-bottom: 0; height: 100%;
} color: #e0def4;
iframe { display: flex;
border: 4px solid #313131; align-items: center;
background-color: #121212; justify-content: center;
border-radius: 1rem; flex-direction: column;
margin: 2em; input,
margin-top: 0.5em; button {
width: calc(100% - 4em); font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont,
height: calc(100% - 8em); sans-serif;
} }
h1 {
input.bar { font-family: "Inter Tight", "Inter", system-ui, -apple-system, BlinkMacSystemFont,
border: none; sans-serif;
outline: none; margin-bottom: 0;
color: #fff; }
height: 2em; iframe {
width: 60%; border: 4px solid #313131;
text-align: center; background-color: #121212;
border-radius: 0.75em; border-radius: 1rem;
background-color: #313131; margin: 2em;
padding: 0.45em; margin-top: 0.5em;
} width: calc(100% - 4em);
.cfg * { height: calc(100% - 8em);
margin: 2px; }
}
.buttons button { input.bar {
border: 2px solid #4c8bf5; border: none;
background-color: #313131; outline: none;
border-radius: 0.75em; color: #fff;
color: #fff; height: 2em;
padding: 0.45em; width: 60%;
} text-align: center;
.cfg input { border-radius: 0.75em;
border: none; background-color: #313131;
background-color: #313131; padding: 0.45em;
border-radius: 0.75em; }
color: #fff; .cfg * {
outline: none; margin: 2px;
padding: 0.45em; }
} .buttons button {
`; border: 2px solid #4c8bf5;
background-color: #313131;
return html` border-radius: 0.75em;
<div> color: #fff;
<h1>Percury Unblocker</h1> padding: 0.45em;
<p>surf the unblocked and mostly buggy web</p> }
.cfg input {
<div class=${`${flex} ${col} cfg`}> border: none;
<input bind:value=${use(store.wispurl)}></input> background-color: #313131;
<input bind:value=${use(store.bareurl)}></input> border-radius: 0.75em;
color: #fff;
outline: none;
<div class=${`${flex} buttons`}> padding: 0.45em;
<button on:click=${() => connection.setTransport("/baremod/index.mjs", [store.bareurl])}>use bare server 3</button> }
<button on:click=${() => connection.setTransport("/libcurl/index.mjs", [{ wisp: store.wispurl }])}>use libcurl.js</button> `;
<button on:click=${() => connection.setTransport("/epoxy/index.mjs", [{ wisp: store.wispurl }])}>use epoxy</button>
<button on:click=${() => window.open(this.urlencoded)}>open in fullscreen</button> return html`
</div> <div>
</div> <h1>Percury Unblocker</h1>
<input class="bar" bind:value=${use(store.url)} on:input=${(e) => (store.url = e.target.value)} on:keyup=${(e) => e.keyCode == 13 && console.log(this.urlencoded = __scramjet$config.prefix + __scramjet$config.codec.encode(e.target.value))}></input> <p>surf the unblocked and mostly buggy web</p>
<iframe src=${use(this.urlencoded)}></iframe>
</div> <div class=${`${flex} ${col} cfg`}>
` <input bind:value=${use(store.wispurl)}></input>
} <input bind:value=${use(store.bareurl)}></input>
window.addEventListener("load", () => {
document.body.appendChild(h(App)) <div class=${`${flex} buttons`}>
}) <button on:click=${() => connection.setTransport("/baremod/index.mjs", [store.bareurl])}>use bare server 3</button>
<button on:click=${() => connection.setTransport("/libcurl/index.mjs", [{ wisp: store.wispurl }])}>use libcurl.js</button>
<button on:click=${() => connection.setTransport("/epoxy/index.mjs", [{ wisp: store.wispurl }])}>use epoxy</button>
<button on:click=${() => window.open(this.urlencoded)}>open in fullscreen</button>
</div>
</div>
<input class="bar" bind:value=${use(store.url)} on:input=${(e) => (store.url = e.target.value)} on:keyup=${(e) => e.keyCode == 13 && console.log((this.urlencoded = $scramjet.config.prefix + $scramjet.config.codec.encode(e.target.value)))}></input>
<iframe src=${use(this.urlencoded)}></iframe>
</div>
`;
}
window.addEventListener("load", () => {
document.body.appendChild(h(App));
});

7
tests/location.html Normal file
View file

@ -0,0 +1,7 @@
<head></head>
<script>
function f() {
location = "http://www.google.com";
}
</script>
<button onclick="f()">Google</button>

View file

@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
// "allowJs": true, // "allowJs": true,
"rootDir": "./src", "rootDir": "./src",
"target": "ES2022", "target": "ES2022",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"module": "ES2022", "module": "ES2022"
}, },
"include": ["src"], "include": ["src"]
} }