chemicaljs 2.6.3 → 3.0.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/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ node_modules
2
+
3
+ # Output
4
+ .output
5
+ .vercel
6
+ .netlify
7
+ .wrangler
8
+ /.svelte-kit
9
+ /build
10
+
11
+ # OS
12
+ .DS_Store
13
+ Thumbs.db
14
+
15
+ # Env
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+ !.env.test
20
+
21
+ # Vite
22
+ vite.config.js.timestamp-*
23
+ vite.config.ts.timestamp-*
@@ -0,0 +1,166 @@
1
+ import * as Scramjet from "@mercuryworkshop/scramjet";
2
+ import * as ScramjetController from "@mercuryworkshop/scramjet-controller";
3
+
4
+ const { BareResponse } = Scramjet;
5
+ const { ManagedPlugin } = ScramjetController;
6
+
7
+ export class AdblockPlugin extends ManagedPlugin {
8
+ constructor(options = {}) {
9
+ super("scramjet-adblock", []);
10
+ this.enabled = options.enabled ?? true;
11
+ this.blockedStatus = options.blockedStatus ?? 204;
12
+ this.onBlock = options.onBlock;
13
+ this._blockedCount = 0;
14
+ this.blockedDomains = new Set();
15
+ this.patternRules = [];
16
+
17
+ if (options.rules) {
18
+ this.addRules(options.rules);
19
+ }
20
+
21
+ if (options.rawRulesets) {
22
+ for (const raw of options.rawRulesets) {
23
+ this.parseAndAddRawRuleset(raw);
24
+ }
25
+ }
26
+ }
27
+
28
+ get blockedCount() {
29
+ return this._blockedCount;
30
+ }
31
+
32
+ resetCount() {
33
+ this._blockedCount = 0;
34
+ }
35
+
36
+ addRules(rules) {
37
+ for (const rawRule of rules) {
38
+ const rule = rawRule.trim().toLowerCase();
39
+ if (!rule || rule.startsWith("#") || rule.startsWith("!")) continue;
40
+
41
+ if (rule.startsWith("/") && rule.endsWith("/") && rule.length > 2) {
42
+ try {
43
+ this.patternRules.push({
44
+ raw: rule,
45
+ regex: new RegExp(rule.slice(1, -1), "i"),
46
+ });
47
+ } catch (err) {
48
+ console.warn(`[scramjet-adblock] Invalid regex rule "${rule}":`, err);
49
+ }
50
+ continue;
51
+ }
52
+
53
+ if (rule.includes("*") || rule.includes("?")) {
54
+ const regexStr = rule
55
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
56
+ .replace(/\*/g, ".*")
57
+ .replace(/\?/g, ".");
58
+ this.patternRules.push({
59
+ raw: rule,
60
+ regex: new RegExp(`^${regexStr}$`, "i"),
61
+ });
62
+ continue;
63
+ }
64
+
65
+ this.blockedDomains.add(rule);
66
+ }
67
+ }
68
+
69
+ parseAndAddRawRuleset(content) {
70
+ const lines = content.split(/\r?\n/);
71
+ const extracted = [];
72
+
73
+ for (let line of lines) {
74
+ line = line.trim();
75
+ if (!line || line.startsWith("#") || line.startsWith("!")) continue;
76
+
77
+ if (line.startsWith("127.0.0.1") || line.startsWith("0.0.0.0")) {
78
+ const parts = line.split(/\s+/);
79
+ if (parts.length >= 2 && parts[1] !== "localhost") {
80
+ extracted.push(parts[1]);
81
+ }
82
+ continue;
83
+ }
84
+
85
+ if (line.startsWith("||")) {
86
+ const domain = line.slice(2).replace(/\^.*$/, "");
87
+ if (domain) extracted.push(domain);
88
+ continue;
89
+ }
90
+
91
+ extracted.push(line);
92
+ }
93
+
94
+ this.addRules(extracted);
95
+ }
96
+
97
+ removeRule(rule) {
98
+ const clean = rule.trim().toLowerCase();
99
+ this.blockedDomains.delete(clean);
100
+ this.patternRules = this.patternRules.filter((r) => r.raw !== clean);
101
+ }
102
+
103
+ clearRules() {
104
+ this.blockedDomains.clear();
105
+ this.patternRules = [];
106
+ }
107
+
108
+ isBlocked(url) {
109
+ try {
110
+ const parsed = new URL(url);
111
+ const hostname = parsed.hostname.toLowerCase();
112
+ const fullUrl = parsed.href.toLowerCase();
113
+
114
+ if (this.blockedDomains.has(hostname)) {
115
+ return { blocked: true, matchedRule: hostname };
116
+ }
117
+
118
+ const parts = hostname.split(".");
119
+ for (let i = 1; i < parts.length - 1; i++) {
120
+ const parentDomain = parts.slice(i).join(".");
121
+ if (this.blockedDomains.has(parentDomain)) {
122
+ return { blocked: true, matchedRule: parentDomain };
123
+ }
124
+ }
125
+
126
+ for (const { raw, regex } of this.patternRules) {
127
+ if (regex.test(hostname) || regex.test(fullUrl)) {
128
+ return { blocked: true, matchedRule: raw };
129
+ }
130
+ }
131
+ } catch {}
132
+
133
+ return { blocked: false };
134
+ }
135
+
136
+ install(frame) {
137
+ super.install(frame);
138
+
139
+ const hooks = frame.fetchHandler.hooks.fetch;
140
+
141
+ this.tap(hooks.request, async (ctx, props) => {
142
+ if (!this.enabled) return;
143
+ if (props.earlyResponse) return;
144
+
145
+ const targetUrl = ctx.parsed.url.href;
146
+ const { blocked, matchedRule } = this.isBlocked(targetUrl);
147
+
148
+ if (blocked) {
149
+ this._blockedCount++;
150
+ if (this.onBlock && matchedRule) {
151
+ this.onBlock(targetUrl, matchedRule);
152
+ }
153
+
154
+ props.earlyResponse = BareResponse.fromNativeResponse(
155
+ new Response(null, {
156
+ status: this.blockedStatus,
157
+ statusText: "Blocked by Scramjet Adblock",
158
+ headers: new Headers({
159
+ "x-scramjet-blocked-by": "adblock-plugin",
160
+ }),
161
+ }),
162
+ );
163
+ }
164
+ });
165
+ }
166
+ }
@@ -0,0 +1,248 @@
1
+ <script>
2
+ import EpoxyTransport from "@mercuryworkshop/epoxy-transport";
3
+ import LibcurlClient from "@mercuryworkshop/libcurl-transport";
4
+
5
+ let {
6
+ transport = $bindable("epoxy"),
7
+ wisp = $bindable(
8
+ (location.protocol === "https:" ? "wss" : "ws") +
9
+ "://" +
10
+ location.host +
11
+ "/wisp/",
12
+ ),
13
+ ready = $bindable(false),
14
+ onready,
15
+ } = $props();
16
+
17
+ let defaultConfig;
18
+ let rewriteUrl;
19
+ let Controller;
20
+ let HttpCachePlugin;
21
+ let UrlWatcherPlugin;
22
+ let CatchEscapedLinksPlugin;
23
+ let EventHandlerPlugin;
24
+ let LinkHandlerPlugin;
25
+ let AdblockPlugin;
26
+
27
+ let controller = $state();
28
+ let scramjetLoaded = $state(false);
29
+ let controllerLoaded = $state(false);
30
+ let currentTransport = transport;
31
+ let currentWisp = wisp;
32
+
33
+ $effect(() => {
34
+ if (scramjetLoaded && controllerLoaded) {
35
+ initialize();
36
+ }
37
+ });
38
+
39
+ async function createTransport() {
40
+ let newTransport;
41
+
42
+ switch (transport) {
43
+ case "epoxy":
44
+ newTransport = new EpoxyTransport({
45
+ wisp,
46
+ });
47
+ break;
48
+ case "libcurl":
49
+ newTransport = new LibcurlClient({
50
+ wisp,
51
+ });
52
+ break;
53
+ default:
54
+ throw new Error("Invalid transport");
55
+ break;
56
+ }
57
+
58
+ await newTransport.init();
59
+
60
+ return newTransport;
61
+ }
62
+
63
+ $effect(async () => {
64
+ if (ready) {
65
+ if (transport !== currentTransport || wisp !== currentWisp) {
66
+ let newTransport = await createTransport();
67
+
68
+ controller.transport = newTransport;
69
+ currentTransport = transport;
70
+ currentWisp = wisp;
71
+ }
72
+ }
73
+ });
74
+
75
+ async function initialize() {
76
+ [
77
+ { defaultConfig, rewriteUrl },
78
+ { Controller },
79
+ {
80
+ HttpCachePlugin,
81
+ UrlWatcherPlugin,
82
+ CatchEscapedLinksPlugin,
83
+ EventHandlerPlugin,
84
+ LinkHandlerPlugin,
85
+ },
86
+ { AdblockPlugin },
87
+ ] = await Promise.all([
88
+ import("@mercuryworkshop/scramjet"),
89
+ import("@mercuryworkshop/scramjet-controller"),
90
+ import("@mercuryworkshop/scramjet-utils"),
91
+ import("./AdblockPlugin.js"),
92
+ ]);
93
+
94
+ await navigator.serviceWorker.register("/sw.js");
95
+
96
+ const registration = await navigator.serviceWorker.ready;
97
+
98
+ const serviceworker =
99
+ navigator.serviceWorker.controller ?? registration.active;
100
+
101
+ let newTransport = await createTransport();
102
+
103
+ controller = new Controller({
104
+ serviceworker,
105
+ transport: newTransport,
106
+ scramjetConfig: defaultConfig,
107
+ });
108
+
109
+ await controller.wait();
110
+
111
+ ready = true;
112
+
113
+ if (onready) {
114
+ onready();
115
+ }
116
+ }
117
+
118
+ export function url(url, plugins = []) {
119
+ if (ready) {
120
+ let newFrame = controller.createFrame(null, plugins);
121
+
122
+ return rewriteUrl(url, newFrame.context, {
123
+ origin: new URL(location.href),
124
+ base: new URL(location.href),
125
+ });
126
+ }
127
+ }
128
+
129
+ export function createFrame(...props) {
130
+ return controller.createFrame(...props);
131
+ }
132
+
133
+ export const plugins = {
134
+ get HttpCachePlugin() {
135
+ return HttpCachePlugin;
136
+ },
137
+ get UrlWatcherPlugin() {
138
+ return UrlWatcherPlugin;
139
+ },
140
+ get CatchEscapedLinksPlugin() {
141
+ return CatchEscapedLinksPlugin;
142
+ },
143
+ get EventHandlerPlugin() {
144
+ return EventHandlerPlugin;
145
+ },
146
+ get LinkHandlerPlugin() {
147
+ return LinkHandlerPlugin;
148
+ },
149
+ get AdblockPlugin() {
150
+ return AdblockPlugin;
151
+ },
152
+ };
153
+
154
+ function fetch(...props) {
155
+ if (transport === "epoxy") {
156
+ return controller.transport.client.fetch(...props);
157
+ } else if (transport === "libcurl") {
158
+ return controller.transport.session.fetch(...props);
159
+ }
160
+ }
161
+
162
+ async function autocomplete(query, engine = "google") {
163
+ if (!query || !query.trim()) return [];
164
+
165
+ const q = encodeURIComponent(query.trim());
166
+
167
+ const endpoints = {
168
+ google: `https://suggestqueries.google.com/complete/search?client=firefox&q=${q}`,
169
+ duckduckgo: `https://duckduckgo.com/ac/?q=${q}`,
170
+ bing: `https://api.bing.com/osjson.aspx?query=${q}`,
171
+ brave: `https://search.brave.com/api/suggest?q=${q}`,
172
+ yahoo: `https://ff.search.yahoo.com/gossip?output=fxjson&command=${q}`,
173
+ };
174
+
175
+ const url = endpoints[engine];
176
+
177
+ if (!url) {
178
+ throw new Error(
179
+ `Unsupported engine "${engine}". Supported: google, duckduckgo, bing, brave, yahoo`,
180
+ );
181
+ }
182
+
183
+ try {
184
+ const response = await fetch(url);
185
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
186
+
187
+ const data = await response.json();
188
+
189
+ switch (engine) {
190
+ case "google":
191
+ return data[1];
192
+ case "duckduckgo":
193
+ return data.map((item) => item.phrase);
194
+ case "bing":
195
+ return data[1];
196
+ case "brave":
197
+ return data[1];
198
+ case "yahoo":
199
+ return data[1];
200
+ }
201
+ } catch (error) {
202
+ console.error(`Autocomplete error (${engine}):`, error);
203
+ return [];
204
+ }
205
+ }
206
+
207
+ async function createDataURL(url) {
208
+ return new Promise(async (resolve, reject) => {
209
+ try {
210
+ const response = await fetch(url);
211
+ const blob = await response.blob();
212
+ const reader = new FileReader();
213
+
214
+ reader.onloadend = function () {
215
+ resolve(reader.result);
216
+ };
217
+
218
+ reader.readAsDataURL(blob);
219
+ } catch {
220
+ resolve(undefined);
221
+ }
222
+ });
223
+ }
224
+
225
+ const searchEngines = {
226
+ google: "https://www.google.com/search?q=%s",
227
+ duckduckgo: "https://duckduckgo.com/?q=%s",
228
+ ddg: "https://duckduckgo.com/?q=%s",
229
+ bing: "https://www.bing.com/search?q=%s",
230
+ brave: "https://search.brave.com/search?q=%s",
231
+ yahoo: "https://search.yahoo.com/search?p=%s",
232
+ };
233
+
234
+ export { fetch, autocomplete, createDataURL, searchEngines };
235
+ </script>
236
+
237
+ <svelte:head>
238
+ <script
239
+ src="./scramjet/scramjet.js"
240
+ onload={() => (scramjetLoaded = true)}
241
+ ></script>
242
+ {#if scramjetLoaded}
243
+ <script
244
+ src="./controller/controller.api.js"
245
+ onload={() => (controllerLoaded = true)}
246
+ ></script>
247
+ {/if}
248
+ </svelte:head>
package/Frame.svelte ADDED
@@ -0,0 +1,102 @@
1
+ <script>
2
+ import blacklistText from "./blacklist.txt?raw";
3
+
4
+ let {
5
+ ready = $bindable(),
6
+ chemical = $bindable(),
7
+ class: className,
8
+ src = $bindable(),
9
+ onchange,
10
+ cache = false,
11
+ linkhandler,
12
+ catchlinks,
13
+ adblock = false,
14
+ ...restProps
15
+ } = $props();
16
+
17
+ let newSrc = $state();
18
+
19
+ let frame;
20
+ let frameRef = $state();
21
+
22
+ $effect(() => {
23
+ if (ready && chemical) {
24
+ let plugins = [];
25
+
26
+ if (onchange) {
27
+ const urlWatcher = new chemical.plugins.UrlWatcherPlugin((url) =>
28
+ onchange(url),
29
+ );
30
+ plugins.push(urlWatcher);
31
+ }
32
+
33
+ if (cache) {
34
+ const cachePlugin = new chemical.plugins.HttpCachePlugin();
35
+ plugins.push(cachePlugin);
36
+ }
37
+
38
+ if (linkhandler) {
39
+ plugins.push(new chemical.plugins.EventHandlerPlugin());
40
+ const linkHandler = new chemical.plugins.LinkHandlerPlugin((url) =>
41
+ linkhandler(url),
42
+ );
43
+ plugins.push(linkHandler);
44
+ }
45
+
46
+ if (catchlinks) {
47
+ const catchEscapedLinks = new chemical.plugins.CatchEscapedLinksPlugin(
48
+ (url) => catchlinks(url),
49
+ );
50
+ plugins.push(catchEscapedLinks);
51
+ }
52
+
53
+ if (adblock) {
54
+ const adblocker = new chemical.plugins.AdblockPlugin({
55
+ rawRulesets: [blacklistText],
56
+ onBlock: (url, rule) => {
57
+ console.log(`[AdBlock] Blocked ${url} matching rule: ${rule}`);
58
+ },
59
+ });
60
+ plugins.push(adblocker);
61
+ }
62
+
63
+ frame = chemical.createFrame(frameRef, {
64
+ plugins,
65
+ });
66
+ frame.go(src);
67
+ }
68
+ });
69
+
70
+ export function go(src, options = {}) {
71
+ let autoHttps = options.autoHttps || false;
72
+ let searchEngine = options.searchEngine;
73
+
74
+ if (frame) {
75
+ if (src.match(/^https?:\/\//)) {
76
+ frame.go(src);
77
+ } else if (
78
+ autoHttps === true &&
79
+ src.includes(".") &&
80
+ !src.includes(" ")
81
+ ) {
82
+ frame.go("https://" + src);
83
+ } else if (searchEngine) {
84
+ frame.go(searchEngine.replace("%s", encodeURIComponent(src)));
85
+ } else {
86
+ frame.go(src);
87
+ }
88
+ }
89
+ }
90
+
91
+ export function reload() {
92
+ frameRef.contentWindow.location.reload();
93
+ }
94
+ export function forward() {
95
+ frameRef.contentWindow.history.forward();
96
+ }
97
+ export function back() {
98
+ frameRef.contentWindow.history.back();
99
+ }
100
+ </script>
101
+
102
+ <iframe bind:this={frameRef} class={className} {...restProps}></iframe>
package/LICENSE CHANGED
@@ -658,4 +658,4 @@ specific requirements.
658
658
  You should also get your employer (if you work as a programmer) or school,
659
659
  if any, to sign a "copyright disclaimer" for the program, if necessary.
660
660
  For more information on this, and how to apply and follow the GNU AGPL, see
661
- <https://www.gnu.org/licenses/>.
661
+ <https://www.gnu.org/licenses/>.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Chemical
2
- Easily create your own web proxy with no experience required.
2
+ Easily create your own web proxy with no experience required. V3 has been simplified to just Scramjet and is now Svelte only.
3
3
 
4
4
  ## Docs
5
5
  You can find documentation for Chemical on our website https://chemicaljs.github.io.
6
6
 
7
7
  ## License
8
- Chemical uses the AGPL 3.0 license.
8
+ Chemical uses the AGPL 3.0 license.