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

View file

@ -1,22 +1,22 @@
# Scramjet
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.
The UI is not finalized and only used as a means to test the web proxy.
## 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`.
## TODO
- Finish HTML 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
- Finish JS rewriting
- Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present
- Write client APIs
- Fix `Illegal Invocation` when calling `addEventListener()` on the window proxy
- Get rid of ESM builds and pollute the global namespace (maybe?)
# Scramjet
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.
The UI is not finalized and only used as a means to test the web proxy.
## 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`.
## TODO
- Finish HTML 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
- Finish JS rewriting
- Check imports/exports for values contained in the `importmap` array, don't rewrite the node value if present
- Write client APIs
- Fix `Illegal Invocation` when calling `addEventListener()` on the window proxy
- Get rid of ESM builds and pollute the global namespace (maybe?)

View file

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

6
lib/index.d.ts vendored
View file

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

View file

@ -1,60 +1,59 @@
{
"name": "@mercuryworkshop/scramjet",
"version": "1.0.2",
"description": "An experimental web proxy that aims to be the successor to Ultraviolet",
"main": "./lib/index.cjs",
"types": "./lib/index.d.js",
"repository": {
"type": "git",
"url": "https://github.com/MercuryWorkshop/scramjet"
},
"scripts": {
"build": "rollup -c",
"dev": "node server.js",
"temp": "rollup -c -w",
"prepublish": "pnpm build",
"pub": "pnpm publish --no-git-checks --access public"
},
"files": [
"dist",
"lib"
],
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@fastify/static": "^7.0.3",
"@mercuryworkshop/bare-as-module3": "^2.2.2",
"@mercuryworkshop/epoxy-transport": "^2.1.3",
"@mercuryworkshop/libcurl-transport": "^1.3.6",
"@rollup/plugin-inject": "^5.0.5",
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-node-resolve": "^15.2.3",
"@tomphttp/bare-server-node": "^2.0.3",
"@types/eslint": "^8.56.10",
"@types/estree": "^1.0.5",
"@types/node": "^20.14.10",
"@types/serviceworker": "^0.0.85",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"fastify": "^4.26.2",
"rollup": "^4.17.2",
"rollup-plugin-typescript2": "^0.36.0",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
},
"type": "module",
"dependencies": {
"@mercuryworkshop/bare-mux": "^2.0.2",
"@webreflection/idb-map": "^0.3.1",
"astravel": "^0.6.1",
"astring": "^1.8.6",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"htmlparser2": "^9.1.0",
"meriyah": "^4.4.2"
}
"name": "@mercuryworkshop/scramjet",
"version": "1.0.2",
"description": "An experimental web proxy that aims to be the successor to Ultraviolet",
"main": "./lib/index.cjs",
"types": "./lib/index.d.js",
"repository": {
"type": "git",
"url": "https://github.com/MercuryWorkshop/scramjet"
},
"scripts": {
"build": "rspack build",
"dev": "node server.js",
"prepublish": "pnpm build",
"pub": "pnpm publish --no-git-checks --access public"
},
"files": [
"dist",
"lib"
],
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@fastify/static": "^7.0.3",
"@mercuryworkshop/bare-as-module3": "^2.2.2",
"@mercuryworkshop/epoxy-transport": "^2.1.3",
"@mercuryworkshop/libcurl-transport": "^1.3.6",
"@rsdoctor/rspack-plugin": "^0.3.7",
"@rspack/cli": "^0.7.5",
"@rspack/core": "^0.7.5",
"@tomphttp/bare-server-node": "^2.0.3",
"@types/eslint": "^8.56.10",
"@types/estree": "^1.0.5",
"@types/node": "^20.14.10",
"@types/serviceworker": "^0.0.85",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"fastify": "^4.26.2",
"prettier": "^3.3.3",
"tslib": "^2.6.2",
"typescript": "^5.4.5"
},
"type": "module",
"dependencies": {
"@mercuryworkshop/bare-mux": "^2.0.2",
"@webreflection/idb-map": "^0.3.1",
"astravel": "^0.6.1",
"astring": "^1.8.6",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"htmlparser2": "^9.1.0",
"meriyah": "^4.4.2",
"parse-domain": "^8.0.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
import { createBareServer } from "@tomphttp/bare-server-node";
import { createServer } from "http";
import Fastify from "fastify";
import fastifyStatic from "@fastify/static";
import { join } from "node:path";
import fs from "node:fs"
import { spawn } from "node:child_process"
import { fileURLToPath } from "node:url";
import { loadConfigFile } from "rollup/loadConfigFile"
//transports
import { baremuxPath } from "@mercuryworkshop/bare-mux/node"
import { epoxyPath } from "@mercuryworkshop/epoxy-transport"
import { libcurlPath } from "@mercuryworkshop/libcurl-transport"
import { bareModulePath } from "@mercuryworkshop/bare-as-module3"
const bare = createBareServer("/bare/", {
logErrors: true
});
const fastify = Fastify({
serverFactory: (handler) => {
return createServer()
.on("request", (req, res) => {
if (bare.shouldRoute(req)) {
bare.routeRequest(req, res);
} else {
handler(req, res);
}
}).on("upgrade", (req, socket, head) => {
if (bare.shouldRoute(req)) {
bare.routeUpgrade(req, socket, head);
} else {
socket.end();
}
})
}
});
fastify.register(fastifyStatic, {
root: join(fileURLToPath(new URL(".", import.meta.url)), "./static"),
decorateReply: false
});
fastify.register(fastifyStatic, {
root: join(fileURLToPath(new URL(".", import.meta.url)), "./dist"),
prefix: "/scram/",
decorateReply: false
})
fastify.register(fastifyStatic, {
root: baremuxPath,
prefix: "/baremux/",
decorateReply: false
})
fastify.register(fastifyStatic, {
root: epoxyPath,
prefix: "/epoxy/",
decorateReply: false
})
fastify.register(fastifyStatic, {
root: libcurlPath,
prefix: "/libcurl/",
decorateReply: false
})
fastify.register(fastifyStatic, {
root: bareModulePath,
prefix: "/baremod/",
decorateReply: false
})
fastify.listen({
port: process.env.PORT || 1337
});
const watch = spawn("rollup", ["-c", "-w"], { detached: true });
watch.stdout.on("data", (data) => {
console.log(`${data}`);
});
watch.stderr.on("data", (data) => {
console.log(`${data}`);
});
// Dev server imports
import { createBareServer } from "@tomphttp/bare-server-node";
import { createServer } from "http";
import Fastify from "fastify";
import fastifyStatic from "@fastify/static";
import { join } from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
//transports
import { baremuxPath } from "@mercuryworkshop/bare-mux/node";
import { epoxyPath } from "@mercuryworkshop/epoxy-transport";
import { libcurlPath } from "@mercuryworkshop/libcurl-transport";
import { bareModulePath } from "@mercuryworkshop/bare-as-module3";
const bare = createBareServer("/bare/", {
logErrors: true,
});
const fastify = Fastify({
serverFactory: (handler) => {
return createServer()
.on("request", (req, res) => {
if (bare.shouldRoute(req)) {
bare.routeRequest(req, res);
} else {
handler(req, res);
}
})
.on("upgrade", (req, socket, head) => {
if (bare.shouldRoute(req)) {
bare.routeUpgrade(req, socket, head);
} else {
socket.end();
}
});
},
});
fastify.register(fastifyStatic, {
root: join(fileURLToPath(new URL(".", import.meta.url)), "./static"),
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: join(fileURLToPath(new URL(".", import.meta.url)), "./dist"),
prefix: "/scram/",
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: baremuxPath,
prefix: "/baremux/",
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: epoxyPath,
prefix: "/epoxy/",
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: libcurlPath,
prefix: "/libcurl/",
decorateReply: false,
});
fastify.register(fastifyStatic, {
root: bareModulePath,
prefix: "/baremod/",
decorateReply: false,
});
fastify.listen({
port: process.env.PORT || 1337,
});
const watch = spawn("pnpm", ["rspack", "-w"], {
detached: true,
cwd: process.cwd(),
});
watch.stdout.on("data", (data) => {
console.log(`${data}`);
});
watch.stderr.on("data", (data) => {
console.log(`${data}`);
});

View file

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

View file

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

View file

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

View file

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

View file

@ -1,38 +1,21 @@
import "./window.ts";
import "./event.ts";
import "./native/eval.ts";
import "./location.ts";
import "./trustedTypes.ts";
import "./requests/fetch.ts";
import "./requests/xmlhttprequest.ts";
import "./requests/websocket.ts"
import "./element.ts";
import "./storage.ts";
import "./css.ts";
import "./history.ts"
import "./worker.ts";
import "./scope.ts";
declare global {
interface Window {
//@ts-ignore scope function cant be typed
__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();
}
import "./scope.ts";
import "./window.ts";
import "./event.ts";
import "./native/eval.ts";
import "./location.ts";
import "./trustedTypes.ts";
import "./requests/fetch.ts";
import "./requests/xmlhttprequest.ts";
import "./requests/websocket.ts";
import "./element.ts";
import "./storage.ts";
import "./css.ts";
import "./history.ts";
import "./worker.ts";
import "./url.ts";
declare global {
interface Window {
$s: any;
}
}

View file

@ -1,25 +1,32 @@
// @ts-nocheck
import { encodeUrl, decodeUrl } from "../shared";
const loc = new URL(decodeUrl(location.href));
loc.assign = (url: string) => location.assign(encodeUrl(url));
loc.reload = () => location.reload();
loc.replace = (url: string) => location.replace(encodeUrl(url));
loc.toString = () => loc.href;
export const locationProxy = new Proxy(window.location, {
get(target, prop) {
return loc[prop];
},
set(obj, prop, value) {
if (prop === "href") {
location.href = encodeUrl(value);
} else {
loc[prop] = value;
}
return true;
}
})
// @ts-nocheck
import { encodeUrl, decodeUrl } from "./shared";
function createLocation() {
const loc = new URL(decodeUrl(location.href));
loc.assign = (url: string) => location.assign(encodeUrl(url));
loc.reload = () => location.reload();
loc.replace = (url: string) => location.replace(encodeUrl(url));
loc.toString = () => loc.href;
return loc;
}
export const locationProxy = new Proxy(window.location, {
get(target, prop) {
const loc = createLocation();
return loc[prop];
},
set(obj, prop, value) {
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";
const FunctionProxy = new Proxy(Function, {
construct(target, argArray) {
if (argArray.length === 1) {
return Reflect.construct(target, rewriteJs(argArray[0]));
} else {
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 {
return Reflect.apply(target, undefined, [...argArray.map((x, index) => index === argArray.length - 1), 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);
import { rewriteJs } from "../shared";
const FunctionProxy = new Proxy(Function, {
construct(target, argArray) {
if (argArray.length === 1) {
return Reflect.construct(target, rewriteJs(argArray[0]));
} else {
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 {
return Reflect.apply(target, undefined, [
...argArray.map((x, index) => index === argArray.length - 1),
rewriteJs(argArray[argArray.length - 1]),
]);
}
},
});
delete window.Function;
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
import { encodeUrl, rewriteHeaders } from "../../shared";
window.fetch = new Proxy(window.fetch, {
apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray);
},
});
Headers = new Proxy(Headers, {
construct(target, argArray, newTarget) {
argArray[0] = rewriteHeaders(argArray[0]);
return Reflect.construct(target, argArray, newTarget);
},
})
Request = new Proxy(Request, {
construct(target, argArray, newTarget) {
if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]);
return Reflect.construct(target, argArray, newTarget);
},
});
Response.redirect = new Proxy(Response.redirect, {
apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray);
},
});
// ts throws an error if you dont do window.fetch
import { encodeUrl, rewriteHeaders } from "../shared";
window.fetch = new Proxy(window.fetch, {
apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray);
},
});
Headers = new Proxy(Headers, {
construct(target, argArray, newTarget) {
argArray[0] = rewriteHeaders(argArray[0]);
return Reflect.construct(target, argArray, newTarget);
},
});
Request = new Proxy(Request, {
construct(target, argArray, newTarget) {
if (typeof argArray[0] === "string") argArray[0] = encodeUrl(argArray[0]);
return Reflect.construct(target, argArray, newTarget);
},
});
Response.redirect = new Proxy(Response.redirect, {
apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray);
},
});

View file

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

View file

@ -1,20 +1,23 @@
import { encodeUrl, rewriteHeaders } from "../../shared";
XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, {
apply(target, thisArg, argArray) {
if (argArray[1]) argArray[1] = encodeUrl(argArray[1]);
return Reflect.apply(target, thisArg, argArray);
},
});
XMLHttpRequest.prototype.setRequestHeader = new Proxy(XMLHttpRequest.prototype.setRequestHeader, {
apply(target, thisArg, argArray) {
let headerObject = Object.fromEntries([argArray]);
headerObject = rewriteHeaders(headerObject);
argArray = Object.entries(headerObject)[0];
return Reflect.apply(target, thisArg, argArray);
},
});
import { encodeUrl, rewriteHeaders } from "../shared";
XMLHttpRequest.prototype.open = new Proxy(XMLHttpRequest.prototype.open, {
apply(target, thisArg, argArray) {
if (argArray[1]) argArray[1] = encodeUrl(argArray[1]);
return Reflect.apply(target, thisArg, argArray);
},
});
XMLHttpRequest.prototype.setRequestHeader = new Proxy(
XMLHttpRequest.prototype.setRequestHeader,
{
apply(target, thisArg, argArray) {
let headerObject = Object.fromEntries([argArray]);
headerObject = rewriteHeaders(headerObject);
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";
function scope(identifier: any) {
if (identifier instanceof Window) {
return windowProxy;
} else if (identifier instanceof Location) {
return locationProxy;
}
if (identifier instanceof Window) {
return windowProxy;
} else if (identifier instanceof Location) {
return locationProxy;
}
return identifier;
return identifier;
}
// 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 { locationProxy } from "./location";
const store = new IDBMapSync(locationProxy.host, {
prefix: "Storage",
durability: "relaxed"
});
await store.sync();
function storageProxy(scope: Storage): Storage {
return new Proxy(scope, {
get(target, prop) {
switch (prop) {
case "getItem":
return (key: string) => {
return store.get(key);
}
case "setItem":
return (key: string, value: string) => {
store.set(key, value);
store.sync();
}
case "removeItem":
return (key: string) => {
store.delete(key);
store.sync();
}
case "clear":
return () => {
store.clear();
store.sync();
}
case "key":
return (index: number) => {
store.keys()[index];
}
case "length":
return store.size;
default:
return store.get(prop);
}
},
//@ts-ignore
set(target, prop, value) {
store.set(prop, value);
store.sync();
},
defineProperty(target, property, attributes) {
store.set(property as string, attributes.value);
return true;
},
})
}
const localStorageProxy = storageProxy(window.localStorage);
const sessionStorageProxy = storageProxy(window.sessionStorage);
delete window.localStorage;
delete window.sessionStorage;
window.localStorage = localStorageProxy;
window.sessionStorage = sessionStorageProxy;
import IDBMapSync from "@webreflection/idb-map/sync";
import { locationProxy } from "./location";
const store = new IDBMapSync(locationProxy.host, {
prefix: "Storage",
durability: "relaxed",
});
await store.sync();
function storageProxy(scope: Storage): Storage {
return new Proxy(scope, {
get(target, prop) {
switch (prop) {
case "getItem":
return (key: string) => {
return store.get(key);
};
case "setItem":
return (key: string, value: string) => {
store.set(key, value);
store.sync();
};
case "removeItem":
return (key: string) => {
store.delete(key);
store.sync();
};
case "clear":
return () => {
store.clear();
store.sync();
};
case "key":
return (index: number) => {
store.keys()[index];
};
case "length":
return store.size;
default:
return store.get(prop);
}
},
//@ts-ignore
set(target, prop, value) {
store.set(prop, value);
store.sync();
},
defineProperty(target, property, attributes) {
store.set(property as string, attributes.value);
return true;
},
});
}
const localStorageProxy = storageProxy(window.localStorage);
const sessionStorageProxy = storageProxy(window.sessionStorage);
delete window.localStorage;
delete window.sessionStorage;
window.localStorage = localStorageProxy;
window.sessionStorage = sessionStorageProxy;

View file

@ -1,32 +1,40 @@
import { rewriteHtml, rewriteJs, encodeUrl } from "../shared";
// @ts-expect-error
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 rewriteHtml(target1(...argArray1));
},
});
}
if (argArray[1].createScript) {
argArray[1].createScript = new Proxy(argArray[1].createScript, {
apply(target1, thisArg1, argArray1) {
return rewriteJs(target1(...argArray1));
},
});
}
if (argArray[1].createScriptURL) {
argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, {
apply(target1, thisArg1, argArray1) {
return encodeUrl(target1(...argArray1));
},
})
}
return Reflect.apply(target, thisArg, argArray);
},
})
// import { rewriteHtml, rewriteJs, encodeUrl } from "./shared";
// 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 rewriteHtml(target1(...argArray1));
// },
// });
// }
//
// if (argArray[1].createScript) {
// argArray[1].createScript = new Proxy(argArray[1].createScript, {
// apply(target1, thisArg1, argArray1) {
// return rewriteJs(target1(...argArray1));
// },
// });
// }
//
// if (argArray[1].createScriptURL) {
// argArray[1].createScriptURL = new Proxy(argArray[1].createScriptURL, {
// apply(target1, thisArg1, argArray1) {
// return encodeUrl(target1(...argArray1));
// },
// });
// }
//
// 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";
export const windowProxy = new Proxy(window, {
get(target, prop) {
const propIsString = typeof prop === "string";
if (propIsString && prop === "location") {
return locationProxy;
} else if (propIsString && ["window", "top", "parent", "self", "globalThis"].includes(prop)) {
return windowProxy;
}
get(target, prop) {
const propIsString = typeof prop === "string";
if (propIsString && prop === "location") {
return locationProxy;
} else if (
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) {
// ensures that no apis are overwritten
if (typeof prop === "string" && ["window", "top", "parent", "self", "globalThis", "location"].includes(prop)) {
return false;
}
const value = Reflect.get(target, prop);
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";
Worker = new Proxy(Worker, {
construct(target, argArray) {
argArray[0] = encodeUrl(argArray[0]);
// target is a reference to the object that you are proxying
// Reflect.construct is just a wrapper for calling target
// you could do new target(...argArray) and it would work the same effectively
return Reflect.construct(target, argArray);
}
})
Worklet.prototype.addModule = new Proxy(Worklet.prototype.addModule, {
apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0])
return Reflect.apply(target, thisArg, argArray);
},
});
window.importScripts = new Proxy(window.importScripts, {
apply(target, thisArg, argArray) {
for (const i in argArray) {
argArray[i] = encodeUrl(argArray[i]);
}
return Reflect.apply(target, thisArg, argArray);
},
});
import { encodeUrl } from "./shared";
Worker = new Proxy(Worker, {
construct(target, argArray) {
argArray[0] = encodeUrl(argArray[0]);
// target is a reference to the object that you are proxying
// Reflect.construct is just a wrapper for calling target
// you could do new target(...argArray) and it would work the same effectively
return Reflect.construct(target, argArray);
},
});
Worklet.prototype.addModule = new Proxy(Worklet.prototype.addModule, {
apply(target, thisArg, argArray) {
argArray[0] = encodeUrl(argArray[0]);
return Reflect.apply(target, thisArg, argArray);
},
});
// broken
// window.importScripts = new Proxy(window.importScripts, {
// apply(target, thisArg, argArray) {
// for (const i in argArray) {
// argArray[i] = encodeUrl(argArray[i]);
// }
// 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";
// for some reason eslint was parsing the type inside of the function params as a variable
export interface Codec {
// eslint-disable-next-line
encode: (str: string | undefined) => string;
// eslint-disable-next-line
decode: (str: string | undefined) => string;
}
const xor = {
encode: (str: string | undefined, key: number = 2) => {
if (!str) return str;
return encodeURIComponent(str.split("").map((e, i) => i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e).join(""));
},
decode: (str: string | undefined, key: number = 2) => {
if (!str) return str;
return decodeURIComponent(str).split("").map((e, i) => i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e).join("");
}
}
const plain = {
encode: (str: string | undefined) => {
if (!str) return str;
return encodeURIComponent(str);
},
decode: (str: string | undefined) => {
if (!str) return str;
return decodeURIComponent(str);
}
}
/*
const aes = {
encode: (str: string | undefined) => {
if (!str) return str;
return encodeURIComponent(enc(str, "dynamic").substring(10));
},
decode: (str: string | undefined) => {
if (!str) return str;
return dec("U2FsdGVkX1" + decodeURIComponent(str), "dynamic");
}
}
*/
const none = {
encode: (str: string | undefined) => str,
decode: (str: string | undefined) => str,
}
const base64 = {
encode: (str: string | undefined) => {
if (!str) return str;
return decodeURIComponent(btoa(str));
},
decode: (str: string | undefined) => {
if (!str) return str;
return atob(str);
}
}
declare global {
interface Window {
__scramjet$codecs: {
none: Codec;
plain: Codec;
base64: Codec;
xor: Codec;
}
}
}
self.__scramjet$codecs = {
none, plain, base64, xor
}
import { enc, dec } from "./aes";
// for some reason eslint was parsing the type inside of the function params as a variable
export interface Codec {
// eslint-disable-next-line
encode: (str: string | undefined) => string;
// eslint-disable-next-line
decode: (str: string | undefined) => string;
}
const xor = {
encode: (str: string | undefined, key: number = 2) => {
if (!str) return str;
return encodeURIComponent(
str
.split("")
.map((e, i) =>
i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e
)
.join("")
);
},
decode: (str: string | undefined, key: number = 2) => {
if (!str) return str;
return decodeURIComponent(str)
.split("")
.map((e, i) => (i % key ? String.fromCharCode(e.charCodeAt(0) ^ key) : e))
.join("");
},
};
const plain = {
encode: (str: string | undefined) => {
if (!str) return str;
return encodeURIComponent(str);
},
decode: (str: string | undefined) => {
if (!str) return str;
return decodeURIComponent(str);
},
};
/*
const aes = {
encode: (str: string | undefined) => {
if (!str) return str;
return encodeURIComponent(enc(str, "dynamic").substring(10));
},
decode: (str: string | undefined) => {
if (!str) return str;
return dec("U2FsdGVkX1" + decodeURIComponent(str), "dynamic");
}
}
*/
const none = {
encode: (str: string | undefined) => str,
decode: (str: string | undefined) => str,
};
const base64 = {
encode: (str: string | undefined) => {
if (!str) return str;
return decodeURIComponent(btoa(str));
},
decode: (str: string | undefined) => {
if (!str) return str;
return atob(str);
},
};
if (!self.$scramjet) {
//@ts-expect-error really dumb workaround
self.$scramjet = {};
}
self.$scramjet.codecs = {
none,
plain,
base64,
xor,
};

View file

@ -1,25 +1,13 @@
import { Codec } from "./codecs";
declare global {
interface Window {
__scramjet$config: {
prefix: string;
codec: Codec
config: string;
shared: string;
worker: string;
client: string;
codecs: string;
}
}
}
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"
}
if (!self.$scramjet) {
//@ts-expect-error really dumb workaround
self.$scramjet = {};
}
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";
export { rewriteCss } from "./rewriters/css";
export { rewriteHtml, rewriteSrcset } from "./rewriters/html";
export { rewriteJs } from "./rewriters/js";
export { rewriteHeaders } from "./rewriters/headers";
export { rewriteWorkers } from "./rewriters/worker"
export { BareClient } from "@mercuryworkshop/bare-mux"
import { encodeUrl, decodeUrl } from "./rewriters/url";
import { rewriteCss } from "./rewriters/css";
import { rewriteHtml, rewriteSrcset } from "./rewriters/html";
import { rewriteJs } from "./rewriters/js";
import { rewriteHeaders } from "./rewriters/headers";
import { rewriteWorkers } from "./rewriters/worker";
import { isScramjetFile } from "./rewriters/html";
import { BareClient } from "@mercuryworkshop/bare-mux";
import { parseDomain } from "parse-domain";
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;
}
if (!self.$scramjet) {
//@ts-expect-error really dumb workaround
self.$scramjet = {};
}
self.$scramjet.shared = {
util: {
isScramjetFile,
parseDomain,
BareClient,
},
url: {
encodeUrl,
decodeUrl,
},
rewrite: {
rewriteCss,
rewriteHtml,
rewriteSrcset,
rewriteJs,
rewriteHeaders,
rewriteWorkers,
},
};

View file

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

View file

@ -1,52 +1,50 @@
import { encodeUrl } from "./url";
import { BareHeaders } from "@mercuryworkshop/bare-mux";
const cspHeaders = [
"cross-origin-embedder-policy",
"cross-origin-opener-policy",
"cross-origin-resource-policy",
"content-security-policy",
"content-security-policy-report-only",
"expect-ct",
"feature-policy",
"origin-isolation",
"strict-transport-security",
"upgrade-insecure-requests",
"x-content-type-options",
"x-download-options",
"x-frame-options",
"x-permitted-cross-domain-policies",
"x-powered-by",
"x-xss-protection",
// 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
"clear-site-data"
"cross-origin-embedder-policy",
"cross-origin-opener-policy",
"cross-origin-resource-policy",
"content-security-policy",
"content-security-policy-report-only",
"expect-ct",
"feature-policy",
"origin-isolation",
"strict-transport-security",
"upgrade-insecure-requests",
"x-content-type-options",
"x-download-options",
"x-frame-options",
"x-permitted-cross-domain-policies",
"x-powered-by",
"x-xss-protection",
// 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
"clear-site-data",
];
const urlHeaders = [
"location",
"content-location",
"referer"
];
const urlHeaders = ["location", "content-location", "referer"];
export function rewriteHeaders(rawHeaders: BareHeaders, origin?: URL) {
const headers = {};
const headers = {};
for (const key in rawHeaders) {
headers[key.toLowerCase()] = rawHeaders[key];
}
for (const key in rawHeaders) {
headers[key.toLowerCase()] = rawHeaders[key];
}
cspHeaders.forEach((header) => {
delete headers[header];
});
cspHeaders.forEach((header) => {
delete headers[header];
});
urlHeaders.forEach((header) => {
if (headers[header])
headers[header] = encodeUrl(headers[header] as string, origin);
});
urlHeaders.forEach((header) => {
if (headers[header])
headers[header] = encodeUrl(headers[header] as string, origin);
});
if (headers["link"]) {
headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) => encodeUrl(match, origin));
}
if (headers["link"]) {
headers["link"] = headers["link"].replace(/<(.*?)>/gi, (match) =>
encodeUrl(match, origin)
);
}
return headers;
}
return headers;
}

View file

@ -4,95 +4,124 @@ import { hasAttrib } from "domutils";
import render from "dom-serializer";
import { encodeUrl } from "./url";
import { rewriteCss } from "./css";
// import { rewriteJs } from "./js";
import { isScramjetFile } from "..";
import { rewriteJs } from "./js";
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) {
const handler = new DomHandler((err, dom) => dom);
const parser = new Parser(handler);
const handler = new DomHandler((err, dom) => dom);
const parser = new Parser(handler);
parser.write(html);
parser.end();
parser.write(html);
parser.end();
return render(traverseParsedHtml(handler.root, origin));
return render(traverseParsedHtml(handler.root, origin));
}
// i need to add the attributes in during rewriting
function traverseParsedHtml(node, origin?: URL) {
/* csp attributes */
for (const cspAttr of ["nonce", "integrity", "csp"]) {
if (hasAttrib(node, cspAttr)) {
node.attribs[`data-${cspAttr}`] = node.attribs[cspAttr];
delete node.attribs[cspAttr];
}
}
/* csp attributes */
for (const cspAttr of ["nonce", "integrity", "csp"]) {
if (hasAttrib(node, cspAttr)) {
node.attribs[`data-${cspAttr}`] = node.attribs[cspAttr];
delete node.attribs[cspAttr];
}
}
/* url attributes */
for (const urlAttr of ["src", "href", "data", "action", "formaction"]) {
if (hasAttrib(node, urlAttr) && !isScramjetFile(node.attribs[urlAttr])) {
const value = node.attribs[urlAttr];
node.attribs[`data-${urlAttr}`] = value;
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);
}
}
/* url attributes */
for (const urlAttr of ["src", "href", "action", "formaction"]) {
if (hasAttrib(node, urlAttr) && !isScramjetFile(node.attribs[urlAttr])) {
const value = node.attribs[urlAttr];
node.attribs[`data-${urlAttr}`] = value;
node.attribs[urlAttr] = encodeUrl(value, origin);
}
}
if (hasAttrib(node, "srcdoc")) node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin);
if (hasAttrib(node, "style")) node.attribs.style = rewriteCss(node.attribs.style, 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 (node.name === "style" && node.children[0] !== undefined) node.children[0].data = rewriteCss(node.children[0].data, origin);
// if (node.name === "script" && /(application|text)\/javascript|importmap|undefined/.test(node.attribs.type) && node.children[0] !== undefined) node.children[0].data = rewriteJs(node.children[0].data, origin);
if (node.name === "meta" && hasAttrib(node, "http-equiv")) {
if (node.attribs["http-equiv"] === "content-security-policy") {
node = {};
} else if (node.attribs["http-equiv"] === "refresh" && node.attribs.content.includes("url")) {
const contentArray = node.attribs.content.split("url=");
contentArray[1] = encodeUrl(contentArray[1].trim(), origin);
node.attribs.content = contentArray.join("url=");
}
}
if (hasAttrib(node, "srcdoc"))
node.attribs.srcdoc = rewriteHtml(node.attribs.srcdoc, origin);
if (hasAttrib(node, "style"))
node.attribs.style = rewriteCss(node.attribs.style, origin);
if (node.name === "head") {
const scramjetScripts = [];
["codecs", "config", "shared", "client"].forEach((script) => {
scramjetScripts.push(new Element("script", {
src: self.__scramjet$config[script],
type: "module",
"data-scramjet": ""
}));
});
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
) {
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) {
for (const childNode in node.childNodes) {
node.childNodes[childNode] = traverseParsedHtml(node.childNodes[childNode], origin);
}
}
node.children.unshift(...scramjetScripts);
}
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) {
const urls = srcset.split(/ [0-9]+x,? ?/g);
if (!urls) return "";
const sufixes = srcset.match(/ [0-9]+x,? ?/g);
if (!sufixes) return "";
const rewrittenUrls = urls.map((url, i) => {
if (url && sufixes[i]) {
return encodeUrl(url, origin) + sufixes[i];
}
});
const urls = srcset.split(/ [0-9]+x,? ?/g);
if (!urls) return "";
const sufixes = srcset.match(/ [0-9]+x,? ?/g);
if (!sufixes) return "";
const rewrittenUrls = urls.map((url, i) => {
if (url && sufixes[i]) {
return encodeUrl(url, origin) + sufixes[i];
}
});
return rewrittenUrls.join("");
}
return rewrittenUrls.join("");
}

View file

@ -16,64 +16,92 @@ import * as ESTree from "estree";
// top
// parent
export function rewriteJs(js: string, origin?: URL) {
try {
const ast = parseModule(js, {
module: 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);
},
try {
const ast = parseModule(js, {
module: true,
webcompat: true,
});
// js rweriting notrdone
MemberExpression: (node: ESTree.MemberExpression) => {
if (node.object.type === "Identifier" && identifierList.includes(node.object.name)) {
node.object.name = `__s(${node.object.name})`;
}
}
});
customTraveler.go(ast);
return generate(ast);
} catch {
console.log(js);
const identifierList = [
"window",
"self",
"globalThis",
"this",
"parent",
"top",
"location",
];
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";
function canParseUrl(url: string, origin?: URL) {
try {
new URL(url, origin);
try {
new URL(url, origin);
return true;
} catch {
return false;
}
return true;
} catch {
return false;
}
}
// something is broken with this but i didn't debug it
export function encodeUrl(url: string, origin?: URL) {
if (!origin) {
origin = new URL(self.__scramjet$config.codec.decode(location.href.slice((location.origin + self.__scramjet$config.prefix).length)));
}
export function encodeUrl(url: string | URL, origin?: URL) {
if (url instanceof URL) {
return url.toString();
}
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);
}
if (!origin) {
origin = new URL(
self.$scramjet.config.codec.decode(
location.href.slice(
(location.origin + self.$scramjet.config.prefix).length
)
)
);
}
// 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
export function decodeUrl(url: string) {
if (/^(#|about|data|mailto|javascript)/.test(url)) {
return url;
} else if (canParseUrl(url)) {
return self.__scramjet$config.codec.decode(url.slice((location.origin + self.__scramjet$config.prefix).length))
} else {
return url;
}
}
export function decodeUrl(url: string | URL) {
if (url instanceof URL) {
return url.toString();
}
if (/^(#|about|data|mailto|javascript)/.test(url)) {
return url;
} else if (canParseUrl(url)) {
return self.$scramjet.config.codec.decode(
url.slice((location.origin + self.$scramjet.config.prefix).length)
);
} else {
return url;
}
}

View file

@ -1,11 +1,12 @@
import { rewriteJs } from "./js";
export function rewriteWorkers(js: string, origin?: URL) {
let str = new String().toString()
//@ts-expect-error
["codecs", "config", "shared", "client"].forEach((script) => {
str += `import "${self.__scramjet$config[script]}"\n`
})
str += rewriteJs(js, origin);
let str = new String().toString()[
//@ts-expect-error
("codecs", "config", "shared", "client")
].forEach((script) => {
str += `import "${self.$scramjet.config[script]}"\n`;
});
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 { encodeUrl, decodeUrl, rewriteCss, rewriteHeaders, rewriteHtml, rewriteJs, rewriteWorkers } from "../shared";
declare global {
interface Window {
ScramjetServiceWorker;
}
}
export default class ScramjetServiceWorker {
client: typeof BareClient.prototype;
config: typeof self.__scramjet$config;
constructor(config = self.__scramjet$config) {
this.client = new 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;
}
async fetch({ request }: FetchEvent) {
const urlParam = new URLSearchParams(new URL(request.url).search);
if (urlParam.has("url")) {
return Response.redirect(encodeUrl(urlParam.get("url"), new URL(urlParam.get("url"))))
}
try {
const url = new URL(decodeUrl(request.url));
const response: BareResponseFetch = await this.client.fetch(url, {
method: request.method,
body: request.body,
headers: request.headers,
credentials: "omit",
mode: request.mode === "cors" ? request.mode : "same-origin",
cache: request.cache,
redirect: request.redirect,
});
let responseBody;
const responseHeaders = rewriteHeaders(response.rawHeaders, url);
if (response.body) {
switch (request.destination) {
case "iframe":
case "document":
if (responseHeaders["content-type"].startsWith("text/html")) {
responseBody = rewriteHtml(await response.text(), url);
} else {
responseBody = response.body;
}
break;
case "script":
responseBody = rewriteJs(await response.text(), url);
break;
case "style":
responseBody = rewriteCss(await response.text(), url);
break;
case "sharedworker":
case "worker":
responseBody = rewriteWorkers(await response.text(), url);
break;
default:
responseBody = response.body;
break;
}
}
// downloads
if (["document", "iframe"].includes(request.destination)) {
const header = responseHeaders["content-disposition"];
// validate header and test for filename
if (!/\s*?((inline|attachment);\s*?)filename=/i.test(header)) {
// 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
const type = /^\s*?attachment/i.test(header)
? "attachment"
: "inline";
// set the filename
const [filename] = new URL(response.finalURL).pathname
.split("/")
.slice(-1);
responseHeaders[
"content-disposition"
] = `${type}; filename=${JSON.stringify(filename)}`;
}
}
if (responseHeaders["accept"] === "text/event-stream") {
responseHeaders["content-type"] = "text/event-stream";
}
if (crossOriginIsolated) {
responseHeaders["Cross-Origin-Embedder-Policy"] = "require-corp";
}
return new Response(responseBody, {
headers: responseHeaders as HeadersInit,
status: response.status,
statusText: response.statusText
})
} catch (err) {
if (!["document", "iframe"].includes(request.destination))
return new Response(undefined, { status: 500 });
console.error(err);
return renderError(err, decodeUrl(request.url));
}
}
}
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-Embedd'er-Policy"] = "require-corp";
}
return new Response(
errorTemplate(
String(err),
fetchedURL
),
{
status: 500,
headers: headers
}
);
}
import { BareResponseFetch } from "@mercuryworkshop/bare-mux";
import IDBMap from "@webreflection/idb-map";
import { ParseResultType } from "parse-domain";
import { parse } from "path";
declare global {
interface Window {
ScramjetServiceWorker;
}
}
self.ScramjetServiceWorker = class ScramjetServiceWorker {
client: typeof self.$scramjet.shared.util.BareClient.prototype;
config: typeof self.$scramjet.config;
constructor(config = self.$scramjet.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;
}
async fetch({ request }: FetchEvent) {
const urlParam = new URLSearchParams(new URL(request.url).search);
const { encodeUrl, decodeUrl } = self.$scramjet.shared.url;
const {
rewriteHeaders,
rewriteHtml,
rewriteJs,
rewriteCss,
rewriteWorkers,
} = self.$scramjet.shared.rewrite;
const { parseDomain } = self.$scramjet.shared.util;
if (urlParam.has("url")) {
return Response.redirect(
encodeUrl(urlParam.get("url"), new URL(urlParam.get("url")))
);
}
try {
const url = new URL(decodeUrl(request.url));
const cookieStore = new IDBMap(url.host, {
durability: "relaxed",
prefix: "Cookies",
});
const response: BareResponseFetch = await this.client.fetch(url, {
method: request.method,
body: request.body,
headers: request.headers,
credentials: "omit",
mode: request.mode === "cors" ? request.mode : "same-origin",
cache: request.cache,
redirect: request.redirect,
//@ts-ignore why the fuck is this not typed mircosoft
duplex: "half",
});
let responseBody;
const responseHeaders = rewriteHeaders(response.rawHeaders, url);
for (const cookie of (responseHeaders["set-cookie"] || []) as string[]) {
let cookieParsed = cookie.split(";").map((x) => x.trim().split("="));
let [key, value] = cookieParsed.shift();
value = value.replace('"', "");
const hostArg = cookieParsed.find((x) => x[0] === "Domain");
cookieParsed = cookieParsed.filter((x) => x[0] !== "Domain");
let host = hostArg ? hostArg[1] : undefined;
if (url.protocol === "http" && cookieParsed.includes(["Secure"]))
continue;
if (
cookieParsed.includes(["SameSite", "None"]) &&
!cookieParsed.includes(["Secure"])
)
continue;
if (host && host !== url.host) {
if (host.startsWith(".")) host = host.slice(1);
const urlDomain = parseDomain(url.hostname);
if (urlDomain.type === ParseResultType.Listed) {
const { subDomains: _, domain, topLevelDomains } = urlDomain;
if (!host.endsWith([domain, ...topLevelDomains].join(".")))
continue;
} else {
continue;
}
const realCookieStore = new IDBMap(host, {
durability: "relaxed",
prefix: "Cookies",
});
realCookieStore.set(key, {
value: value,
args: cookieParsed,
subdomain: true,
});
} else {
cookieStore.set(key, {
value: value,
args: cookieParsed,
subdomain: false,
});
}
}
for (let header in responseHeaders) {
// flatten everything past here
if (responseHeaders[header] instanceof Array)
responseHeaders[header] = responseHeaders[header][0];
}
if (response.body) {
switch (request.destination) {
case "iframe":
case "document":
if (
responseHeaders["content-type"]
?.toString()
?.startsWith("text/html")
) {
responseBody = rewriteHtml(await response.text(), url);
} else {
responseBody = response.body;
}
break;
case "script":
responseBody = rewriteJs(await response.text(), url);
break;
case "style":
responseBody = rewriteCss(await response.text(), url);
break;
case "sharedworker":
case "worker":
responseBody = rewriteWorkers(await response.text(), url);
break;
default:
responseBody = response.body;
break;
}
}
// downloads
if (["document", "iframe"].includes(request.destination)) {
const header = responseHeaders["content-disposition"];
// validate header and test for filename
if (!/\s*?((inline|attachment);\s*?)filename=/i.test(header)) {
// 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
const type = /^\s*?attachment/i.test(header)
? "attachment"
: "inline";
// set the filename
const [filename] = new URL(response.finalURL).pathname
.split("/")
.slice(-1);
responseHeaders["content-disposition"] =
`${type}; filename=${JSON.stringify(filename)}`;
}
}
if (responseHeaders["accept"] === "text/event-stream") {
responseHeaders["content-type"] = "text/event-stream";
}
if (crossOriginIsolated) {
responseHeaders["Cross-Origin-Embedder-Policy"] = "require-corp";
}
return new Response(responseBody, {
headers: responseHeaders as HeadersInit,
status: response.status,
statusText: response.statusText,
});
} catch (err) {
if (!["document", "iframe"].includes(request.destination))
return new Response(undefined, { status: 500 });
console.error(err);
return renderError(err, decodeUrl(request.url));
}
}
};
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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="prefetch" href="/scram/scramjet.worker.js" type="module">
<link rel="prefetch" href="/scram/scramjet.shared.js" type="module">
<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">
<style>
body, html, #app {
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
width:100vw;
height:100vh;
margin: 0;
padding: 0;
background-color:#121212;
overflow: hidden;
}
</style>
</head>
<body>
<script src="https://unpkg.com/dreamland"></script>
<script src="/baremux/index.js" defer></script>
<script src="/scram/scramjet.codecs.js" type="module"></script>
<script src="/scram/scramjet.config.js" type="module"></script>
<script src="ui.js" defer></script>
</body>
</html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="prefetch" href="/scram/scramjet.worker.js" />
<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"
/>
<style>
body,
html,
#app {
font-family:
"Inter",
system-ui,
-apple-system,
BlinkMacSystemFont,
sans-serif;
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #121212;
overflow: hidden;
}
</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";
import "/scram/scramjet.codecs.js";
import "/scram/scramjet.config.js";
const scramjet = new ScramjetServiceWorker();
async function handleRequest(event) {
if (scramjet.route(event)) {
return scramjet.fetch(event);
}
return fetch(event.request)
}
self.addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event));
});
importScripts(
"/scram/scramjet.codecs.js",
"/scram/scramjet.config.js",
"/scram/scramjet.shared.js",
"/scram/scramjet.worker.js"
);
const scramjet = new ScramjetServiceWorker();
async function handleRequest(event) {
if (scramjet.route(event)) {
return scramjet.fetch(event);
}
return fetch(event.request);
}
self.addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event));
});

View file

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