chemicaljs 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Chemical
2
- Create your own web proxy using Ultraviolet fast, easy, and no experience required.
2
+ Easily create your own web proxy with no experience required.
3
3
 
4
4
  ## Setup
5
5
 
@@ -19,6 +19,17 @@ import { ChemicalServer } from "chemicaljs";
19
19
  const chemical = new ChemicalServer();
20
20
  ```
21
21
 
22
+ You can pass options to disable proxy services and set the default service.
23
+
24
+ ```js
25
+ const chemical = new ChemicalServer({
26
+ default: "rammerhead",
27
+ uv: true,
28
+ scramjet: false,
29
+ rammerhead: true
30
+ });
31
+ ```
32
+
22
33
  3. Use `chemical.app` which is an express app. You may need to import express for certain APIs.
23
34
 
24
35
  ```js
@@ -62,10 +73,22 @@ In your project create a folder to store your static assets. Create an index.htm
62
73
  <script src="/chemical.js"></script>
63
74
  ```
64
75
 
65
- 2. In a inline script or javascript file, encode a URL with Chemical using `window.chemicalEncode`.
76
+ If you want to set the wisp server to an external server just change the `wisp` attribute.
77
+
78
+ ```html
79
+ <script wisp="wss://wisp.mercurywork.shop" src="/chemical.js"></script>
80
+ ```
81
+
82
+ 2. In a inline script or javascript file, encode a URL with Chemical using the async function `window.chemicalEncode`.
83
+
84
+ ```js
85
+ await window.chemicalEncode("https://example.com")
86
+ ```
87
+
88
+ Optional: Change service to `ultraviolet`, `scramjet`, or `rammerhead`. Defaults to `uv` or server option.
66
89
 
67
90
  ```js
68
- window.chemicalEncode("https://example.com")
91
+ await window.chemicalEncode("https://example.com", "rammerhead")
69
92
  ```
70
93
 
71
94
  3. You may want to check if Chemical has loaded before encoding a URL.
@@ -92,9 +115,9 @@ Below is a simple example of a simple input that redirects to the encoded URL wh
92
115
  <script>
93
116
  const search = document.getElementById("search");
94
117
 
95
- search.addEventListener("keydown", function (e) {
118
+ search.addEventListener("keydown", async function (e) {
96
119
  if (e.key == "Enter" && window.chemicalLoaded && e.target.value) {
97
- window.location = window.chemicalEncode(e.target.value)
120
+ window.location = await window.chemicalEncode(e.target.value)
98
121
  }
99
122
  })
100
123
  </script>
@@ -104,7 +127,6 @@ Below is a simple example of a simple input that redirects to the encoded URL wh
104
127
 
105
128
  Some features that may come in the future are:
106
129
 
107
- - More proxy services.
108
130
  - Easy client components.
109
131
  - Easy tab cloaking components.
110
132
 
@@ -0,0 +1,245 @@
1
+ window.chemicalLoaded = false;
2
+
3
+ function rammerheadEncode(baseUrl) {
4
+ const mod = (n, m) => ((n % m) + m) % m
5
+ const baseDictionary =
6
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~-"
7
+ const shuffledIndicator = "_rhs"
8
+ const generateDictionary = function () {
9
+ let str = ""
10
+ const split = baseDictionary.split("")
11
+ while (split.length > 0) {
12
+ str += split.splice(Math.floor(Math.random() * split.length), 1)[0]
13
+ }
14
+ return str
15
+ }
16
+ class StrShuffler {
17
+ constructor(dictionary = generateDictionary()) {
18
+ this.dictionary = dictionary
19
+ }
20
+ shuffle(str) {
21
+ if (str.startsWith(shuffledIndicator)) {
22
+ return str
23
+ }
24
+ let shuffledStr = ""
25
+ for (let i = 0; i < str.length; i++) {
26
+ const char = str.charAt(i)
27
+ const idx = baseDictionary.indexOf(char)
28
+ if (char === "%" && str.length - i >= 3) {
29
+ shuffledStr += char
30
+ shuffledStr += str.charAt(++i)
31
+ shuffledStr += str.charAt(++i)
32
+ } else if (idx === -1) {
33
+ shuffledStr += char
34
+ } else {
35
+ shuffledStr += this.dictionary.charAt(
36
+ mod(idx + i, baseDictionary.length)
37
+ )
38
+ }
39
+ }
40
+ return shuffledIndicator + shuffledStr
41
+ }
42
+ unshuffle(str) {
43
+ if (!str.startsWith(shuffledIndicator)) {
44
+ return str
45
+ }
46
+
47
+ str = str.slice(shuffledIndicator.length)
48
+
49
+ let unshuffledStr = ""
50
+ for (let i = 0; i < str.length; i++) {
51
+ const char = str.charAt(i)
52
+ const idx = this.dictionary.indexOf(char)
53
+ if (char === "%" && str.length - i >= 3) {
54
+ unshuffledStr += char
55
+ unshuffledStr += str.charAt(++i)
56
+ unshuffledStr += str.charAt(++i)
57
+ } else if (idx === -1) {
58
+ unshuffledStr += char
59
+ } else {
60
+ unshuffledStr += baseDictionary.charAt(
61
+ mod(idx - i, baseDictionary.length)
62
+ )
63
+ }
64
+ }
65
+ return unshuffledStr
66
+ }
67
+ }
68
+ function get(url, callback, shush = false) {
69
+ var request = new XMLHttpRequest()
70
+ request.open("GET", url, true)
71
+ request.send()
72
+
73
+ request.onerror = function () {
74
+ if (!shush) console.log("Cannot communicate with the server")
75
+ }
76
+ request.onload = function () {
77
+ if (request.status === 200) {
78
+ callback(request.responseText)
79
+ } else {
80
+ if (!shush)
81
+ console.log(
82
+ 'unexpected server response to not match "200". Server says "' +
83
+ request.responseText +
84
+ '"'
85
+ )
86
+ }
87
+ }
88
+ }
89
+ var api = {
90
+ newsession(callback) {
91
+ get("/newsession", callback)
92
+ },
93
+ sessionexists(id, callback) {
94
+ get("/sessionexists?id=" + encodeURIComponent(id), function (res) {
95
+ if (res === "exists") return callback(true)
96
+ if (res === "not found") return callback(false)
97
+ console.log("unexpected response from server. received" + res)
98
+ })
99
+ },
100
+ shuffleDict(id, callback) {
101
+ console.log("Shuffling", id)
102
+ get("/api/shuffleDict?id=" + encodeURIComponent(id), function (res) {
103
+ callback(JSON.parse(res))
104
+ })
105
+ }
106
+ }
107
+ var localStorageKey = "rammerhead_sessionids"
108
+ var localStorageKeyDefault = "rammerhead_default_sessionid"
109
+ var sessionIdsStore = {
110
+ get() {
111
+ var rawData = localStorage.getItem(localStorageKey)
112
+ if (!rawData) return []
113
+ try {
114
+ var data = JSON.parse(rawData)
115
+ if (!Array.isArray(data)) throw "getout"
116
+ return data
117
+ } catch (e) {
118
+ return []
119
+ }
120
+ },
121
+ set(data) {
122
+ if (!data || !Array.isArray(data)) throw new TypeError("must be array")
123
+ localStorage.setItem(localStorageKey, JSON.stringify(data))
124
+ },
125
+ getDefault() {
126
+ var sessionId = localStorage.getItem(localStorageKeyDefault)
127
+ if (sessionId) {
128
+ var data = sessionIdsStore.get()
129
+ data.filter(function (e) {
130
+ return e.id === sessionId
131
+ })
132
+ if (data.length) return data[0]
133
+ }
134
+ return null
135
+ },
136
+ setDefault(id) {
137
+ localStorage.setItem(localStorageKeyDefault, id)
138
+ }
139
+ }
140
+ function addSession(id) {
141
+ var data = sessionIdsStore.get()
142
+ data.unshift({ id: id, createdOn: new Date().toLocaleString() })
143
+ sessionIdsStore.set(data)
144
+ }
145
+ function getSessionId() {
146
+ return new Promise(resolve => {
147
+ var id = localStorage.getItem("session-string")
148
+ api.sessionexists(id, function (value) {
149
+ if (!value) {
150
+ console.log("Session validation failed")
151
+ api.newsession(function (id) {
152
+ addSession(id)
153
+ localStorage.setItem("session-string", id)
154
+ console.log(id)
155
+ console.log("^ new id")
156
+ resolve(id)
157
+ })
158
+ } else {
159
+ resolve(id)
160
+ }
161
+ })
162
+ })
163
+ }
164
+ var ProxyHref
165
+
166
+ return getSessionId().then(id => {
167
+ return new Promise(resolve => {
168
+ api.shuffleDict(id, function (shuffleDict) {
169
+ var shuffler = new StrShuffler(shuffleDict)
170
+ ProxyHref = "/" + id + "/" + shuffler.shuffle(baseUrl)
171
+ resolve(ProxyHref)
172
+ })
173
+ })
174
+ })
175
+ }
176
+
177
+ async function chemicalEncode(url, service = defaultService) {
178
+ switch (service) {
179
+ case "uv":
180
+ if (uvEnabled) {
181
+ return (
182
+ window.location.origin +
183
+ window.__uv$config.prefix +
184
+ window.__uv$config.encodeUrl(url)
185
+ );
186
+ }
187
+ break;
188
+ case "rammerhead":
189
+ if (rammerheadEnabled) {
190
+ return window.location.origin + (await rammerheadEncode(url));
191
+ }
192
+ break;
193
+ case "scramjet":
194
+ if (scramjetEnabled) {
195
+ return (
196
+ window.location.origin +
197
+ window.__scramjet$config.prefix +
198
+ window.__scramjet$config.codec.encode(url)
199
+ );
200
+ }
201
+ break;
202
+ }
203
+ }
204
+
205
+ let wispURL = document.currentScript.getAttribute("wisp");
206
+
207
+ async function registerSW() {
208
+ const connection = new window.BareMux.BareMuxConnection("/baremux/worker.js");
209
+ await connection.setTransport("/epoxy/module.js", [{ wisp: wispURL || (location.protocol === "https:" ? "wss" : "ws") + "://" + location.host + "/wisp/" }]);
210
+
211
+ if ("serviceWorker" in navigator) {
212
+ await navigator.serviceWorker.register("/chemical.sw.js");
213
+ } else {
214
+ console.error("Service worker failed to register.")
215
+ }
216
+ }
217
+
218
+ async function loadScript(src) {
219
+ await new Promise((resolve, reject) => {
220
+ const script = document.createElement("script");
221
+ script.src = src;
222
+ script.onload = () => {
223
+ resolve();
224
+ };
225
+ script.onerror = () => {
226
+ reject();
227
+ };
228
+ document.head.appendChild(script);
229
+ });
230
+ }
231
+
232
+ (async () => {
233
+ await loadScript("/baremux/index.js");
234
+ if (uvEnabled) {
235
+ await loadScript("/uv/uv.bundle.js");
236
+ await loadScript("/uv/uv.config.js");
237
+ }
238
+ if (scramjetEnabled) {
239
+ await loadScript("/scramjet/scramjet.codecs.js");
240
+ await loadScript("/scramjet/scramjet.config.js");
241
+ }
242
+ await registerSW();
243
+ chemicalLoaded = true;
244
+ window.dispatchEvent(new Event("chemicalLoaded"));
245
+ })();
@@ -0,0 +1,35 @@
1
+ if (uvEnabled) {
2
+ importScripts("/uv/uv.bundle.js");
3
+ importScripts("/uv/uv.config.js");
4
+ importScripts(__uv$config.sw || "/uv/uv.sw.js");
5
+ }
6
+ if (scramjetEnabled) {
7
+ importScripts("/scramjet/scramjet.codecs.js");
8
+ importScripts("/scramjet/scramjet.config.js");
9
+ importScripts(__scramjet$config.bundle || "/scramjet/scramjet.bundle.js");
10
+ importScripts(__scramjet$config.worker || "/scramjet/scramjet.worker.js");
11
+ }
12
+
13
+ Object.defineProperty(self, "crossOriginIsolated", { value: true }); // Firefox fix
14
+
15
+ let uv, scramjet;
16
+
17
+ if (uvEnabled) {
18
+ uv = new UVServiceWorker();
19
+ }
20
+ if (scramjetEnabled) {
21
+ scramjet = new ScramjetServiceWorker();
22
+ }
23
+
24
+ self.addEventListener("fetch", (event) => {
25
+ event.respondWith(
26
+ (async () => {
27
+ if (uvEnabled && uv.route(event)) {
28
+ return await uv.fetch(event);
29
+ } else if (scramjetEnabled && scramjet.route(event)) {
30
+ return await scramjet.fetch(event);
31
+ }
32
+ return await fetch(event.request);
33
+ })(),
34
+ );
35
+ });
@@ -0,0 +1,9 @@
1
+ self.__scramjet$config = {
2
+ prefix: "/~/sj/",
3
+ codec: self.__scramjet$codecs.plain,
4
+ config: "/scramjet/scramjet.config.js",
5
+ bundle: "/scramjet/scramjet.bundle.js",
6
+ worker: "/scramjet/scramjet.worker.js",
7
+ client: "/scramjet/scramjet.client.js",
8
+ codecs: "/scramjet/scramjet.codecs.js",
9
+ };
@@ -1,5 +1,5 @@
1
1
  self.__uv$config = {
2
- prefix: "/~/",
2
+ prefix: "/~/uv/",
3
3
  encodeUrl: Ultraviolet.codec.xor.encode,
4
4
  decodeUrl: Ultraviolet.codec.xor.decode,
5
5
  handler: "/uv/uv.handler.js",
@@ -5,9 +5,9 @@
5
5
  <script>
6
6
  const search = document.getElementById("search");
7
7
 
8
- search.addEventListener("keydown", function (e) {
8
+ search.addEventListener("keydown", async function (e) {
9
9
  if (e.key == "Enter" && window.chemicalLoaded && e.target.value) {
10
- window.location = window.chemicalEncode(e.target.value)
10
+ window.location = await window.chemicalEncode(e.target.value, "rammerhead")
11
11
  }
12
12
  })
13
13
  </script>
package/index.js CHANGED
@@ -1,25 +1,136 @@
1
1
  import { resolve } from "node:path";
2
+ import { readFileSync } from "node:fs";
2
3
  import { createServer } from "node:http";
3
4
  import express from "express";
4
5
  import wisp from "wisp-server-node";
5
6
  import { baremuxPath } from "@mercuryworkshop/bare-mux/node";
6
7
  import { epoxyPath } from "@mercuryworkshop/epoxy-transport";
7
8
  import { uvPath } from "@titaniumnetwork-dev/ultraviolet";
9
+ import { scramjetPath } from "@mercuryworkshop/scramjet";
10
+ import createRammerhead from "rammerhead/src/server/index.js";
8
11
 
9
12
  class ChemicalServer {
10
- constructor() {
13
+ constructor(options) {
14
+ if (typeof options !== "object" || Array.isArray(options)) {
15
+ options = {}
16
+ console.error("Error: ChemicalServer options invalid.")
17
+ }
18
+
19
+ const rh = createRammerhead()
20
+
21
+ const rammerheadScopes = [
22
+ "/rammerhead.js",
23
+ "/hammerhead.js",
24
+ "/transport-worker.js",
25
+ "/task.js",
26
+ "/iframe-task.js",
27
+ "/worker-hammerhead.js",
28
+ "/messaging",
29
+ "/sessionexists",
30
+ "/deletesession",
31
+ "/newsession",
32
+ "/editsession",
33
+ "/needpassword",
34
+ "/syncLocalStorage",
35
+ "/api/shuffleDict"
36
+ ]
37
+
38
+ const rammerheadSession = /^\/[a-z0-9]{32}/
39
+
40
+ const shouldRouteRh = req => {
41
+ const url = new URL(req.url, "http://0.0.0.0")
42
+ return (
43
+ rammerheadScopes.includes(url.pathname) ||
44
+ rammerheadSession.test(url.pathname)
45
+ )
46
+ }
47
+
48
+ const routeRhRequest = (req, res) => {
49
+ rh.emit("request", req, res)
50
+ }
51
+
52
+ const routeRhUpgrade = (req, socket, head) => {
53
+ rh.emit("upgrade", req, socket, head)
54
+ }
55
+
11
56
  this.server = createServer();
12
57
  this.app = express();
13
- this.app.use(express.static(resolve(import.meta.dirname, "public")));
58
+ this.app.get("/chemical.js", async function (req, res) {
59
+ let chemicalMain = await readFileSync(resolve(import.meta.dirname, "client/chemical.js"), "utf8");
60
+
61
+ if (options.default) {
62
+ if (["uv", "rammerhead", "scramjet"].includes(options.default)) {
63
+ chemicalMain = `const defaultService = "${options.default}";\n\n` + chemicalMain
64
+ } else {
65
+ chemicalMain = `const defaultService = "uv";\n\n` + chemicalMain
66
+ console.error("Error: Chemical default option invalid.")
67
+ }
68
+ }
69
+
70
+ if (options.uv == false) {
71
+ chemicalMain = "const uvEnabled = false;\n" + chemicalMain
72
+ } else {
73
+ chemicalMain = "const uvEnabled = true;\n" + chemicalMain
74
+ }
75
+ if (options.scramjet == false) {
76
+ chemicalMain = "const scramjetEnabled = false;\n" + chemicalMain
77
+ } else {
78
+ chemicalMain = "const scramjetEnabled = true;\n" + chemicalMain
79
+ }
80
+ if (options.rammerhead == false) {
81
+ chemicalMain = "const rammerheadEnabled = false;\n" + chemicalMain
82
+ } else {
83
+ chemicalMain = "const rammerheadEnabled = true;\n" + chemicalMain
84
+ }
85
+
86
+ res.type("application/javascript");
87
+ return res.send(chemicalMain);
88
+ });
89
+ this.app.get("/chemical.sw.js", async function (req, res) {
90
+ let chemicalSW = await readFileSync(resolve(import.meta.dirname, "client/chemical.sw.js"), "utf8");
91
+
92
+ if (options.uv == false) {
93
+ chemicalSW = "const uvEnabled = false;\n\n" + chemicalSW
94
+ } else {
95
+ chemicalSW = "const uvEnabled = true;\n\n" + chemicalSW
96
+ }
97
+ if (options.scramjet == false) {
98
+ chemicalSW = "const scramjetEnabled = false;\n" + chemicalSW
99
+ } else {
100
+ chemicalSW = "const scramjetEnabled = true;\n" + chemicalSW
101
+ }
102
+ if (options.rammerhead == false) {
103
+ chemicalSW = "const rammerheadEnabled = false;\n" + chemicalSW
104
+ } else {
105
+ chemicalSW = "const rammerheadEnabled = true;\n" + chemicalSW
106
+ }
107
+
108
+ res.type("application/javascript");
109
+ return res.send(chemicalSW);
110
+ });
111
+ this.app.use(express.static(resolve(import.meta.dirname, "client")));
14
112
  this.app.use("/baremux/", express.static(baremuxPath));
15
113
  this.app.use("/epoxy/", express.static(epoxyPath));
16
- this.app.use("/uv/", express.static(uvPath));
114
+ if (options.uv !== false) {
115
+ this.app.use("/uv/", express.static(resolve(import.meta.dirname, "config/uv")));
116
+ this.app.use("/uv/", express.static(uvPath));
117
+ }
118
+ if (options.scramjet !== false) {
119
+ this.app.use("/scramjet/", express.static(resolve(import.meta.dirname, "config/scramjet")));
120
+ this.app.use("/scramjet/", express.static(scramjetPath));
121
+ }
17
122
  this.server.on("request", (req, res) => {
18
- this.app(req, res);
123
+ if (options.rammerhead !== false && shouldRouteRh(req)) {
124
+ routeRhRequest(req, res);
125
+ } else {
126
+ this.app(req, res);
127
+ }
19
128
  });
20
129
  this.server.on("upgrade", (req, socket, head) => {
21
130
  if (req.url && req.url.endsWith("/wisp/")) {
22
131
  wisp.routeRequest(req, socket, head);
132
+ } else if (options.rammerhead !== false && shouldRouteRh(req)) {
133
+ routeRhUpgrade(req, socket, head);
23
134
  } else {
24
135
  socket.end();
25
136
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chemicaljs",
3
- "version": "1.0.4",
4
- "description": "Create your own web proxy using Ultraviolet fast, easy, and no experience required.",
3
+ "version": "1.2.0",
4
+ "description": "Easily create your own web proxy with no experience required.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/chemicaljs/chemical.git"
@@ -13,8 +13,10 @@
13
13
  "dependencies": {
14
14
  "@mercuryworkshop/bare-mux": "^2.0.0",
15
15
  "@mercuryworkshop/epoxy-transport": "^2.1.0",
16
+ "@mercuryworkshop/scramjet": "^1.0.2",
16
17
  "@titaniumnetwork-dev/ultraviolet": "^3.2.0",
17
18
  "express": "^4.19.2",
19
+ "rammerhead": "file:rammerhead.tgz",
18
20
  "wisp-server-node": "^1.1.0"
19
21
  }
20
22
  }
package/rammerhead.tgz ADDED
Binary file
@@ -1,41 +0,0 @@
1
- window.chemicalLoaded = false;
2
-
3
- function chemicalEncode(url) {
4
- return window.location.origin + window.__uv$config.prefix + window.__uv$config.encodeUrl(url)
5
- }
6
-
7
- let wispURL = document.currentScript.getAttribute("wisp");
8
-
9
- async function registerSW() {
10
- const connection = new window.BareMux.BareMuxConnection("/baremux/worker.js");
11
- await connection.setTransport("/epoxy/module.js", [{ wisp: wispURL || (location.protocol === "https:" ? "wss" : "ws") + "://" + location.host + "/wisp/" }]);
12
-
13
- if ("serviceWorker" in navigator) {
14
- await navigator.serviceWorker.register("/chemical.sw.js");
15
- } else {
16
- console.error("Service worker failed to register.")
17
- }
18
- }
19
-
20
- async function loadScript(src) {
21
- await new Promise((resolve, reject) => {
22
- const script = document.createElement("script");
23
- script.src = src;
24
- script.onload = () => {
25
- resolve();
26
- };
27
- script.onerror = () => {
28
- reject();
29
- };
30
- document.head.appendChild(script);
31
- });
32
- }
33
-
34
- (async () => {
35
- await loadScript("/baremux/index.js");
36
- await loadScript("/uv/uv.bundle.js");
37
- await loadScript("/uv/uv.config.js");
38
- await registerSW();
39
- chemicalLoaded = true;
40
- window.dispatchEvent(new Event("chemicalLoaded"));
41
- })();
@@ -1,19 +0,0 @@
1
- importScripts("/libcurl/index.js");
2
- importScripts("/uv/uv.bundle.js");
3
- importScripts("/uv/uv.config.js");
4
- importScripts(__uv$config.sw || "/uv/uv.sw.js");
5
-
6
- Object.defineProperty(self, "crossOriginIsolated", { value: true }); // Firefox fix
7
-
8
- const uv = new UVServiceWorker();
9
-
10
- self.addEventListener("fetch", (event) => {
11
- event.respondWith(
12
- (async () => {
13
- if (uv.route(event)) {
14
- return await uv.fetch(event);
15
- }
16
- return await fetch(event.request);
17
- })(),
18
- );
19
- });