Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
QuiteAFancyEmerald
GitHub Repository: QuiteAFancyEmerald/Holy-Unblocker
Path: blob/master/views/uv/uv.sw.js
5227 views
1
/* -----------------------------------------------
2
/* Authors: Titanium Network
3
/* GNU Affero General Public License v3.0: https://www.gnu.org/licenses/agpl-3.0.en.html
4
/* Modified by Yoct to remove __uv$config and serve custom error pages.
5
/* Ultraviolet Service Worker Script (v3.2.7)
6
/* ----------------------------------------------- */
7
8
/*globals __uv$config*/
9
// Users must import the config (and bundle) prior to importing uv.sw.js
10
// This is to allow us to produce a generic bundle with no hard-coded paths.
11
12
/**
13
* @type {import('../uv').UltravioletCtor}
14
*/
15
const Ultraviolet = self.Ultraviolet;
16
17
const cspHeaders = [
18
'cross-origin-embedder-policy',
19
'cross-origin-opener-policy',
20
'cross-origin-resource-policy',
21
'content-security-policy',
22
'content-security-policy-report-only',
23
'expect-ct',
24
'feature-policy',
25
'origin-isolation',
26
'strict-transport-security',
27
'upgrade-insecure-requests',
28
'x-content-type-options',
29
'x-download-options',
30
'x-frame-options',
31
'x-permitted-cross-domain-policies',
32
'x-powered-by',
33
'x-xss-protection',
34
];
35
const emptyMethods = ['GET', 'HEAD'];
36
37
class UVServiceWorker extends Ultraviolet.EventEmitter {
38
constructor(config = self['{{__uv$config}}']) {
39
super();
40
if (!config.prefix) config.prefix = '/service/';
41
this.config = config;
42
/**
43
* @type {InstanceType<Ultraviolet['BareClient']>}
44
*/
45
this.bareClient = new Ultraviolet.BareClient();
46
}
47
/**
48
*
49
* @param {Event & {request: Request}} param0
50
* @returns
51
*/
52
route({ request }) {
53
if (request.url.startsWith(location.origin + this.config.prefix))
54
return true;
55
else return false;
56
}
57
/**
58
*
59
* @param {Event & {request: Request}} param0
60
* @returns
61
*/
62
async fetch({ request }) {
63
/**
64
* @type {string|void}
65
*/
66
let fetchedURL;
67
68
try {
69
if (!request.url.startsWith(location.origin + this.config.prefix))
70
return await fetch(request);
71
72
const ultraviolet = new Ultraviolet(this.config);
73
74
if (typeof this.config.construct === 'function') {
75
this.config.construct(ultraviolet, 'service');
76
}
77
78
const db = await ultraviolet.cookie.db();
79
80
ultraviolet.meta.origin = location.origin;
81
ultraviolet.meta.base = ultraviolet.meta.url = new URL(
82
ultraviolet.sourceUrl(request.url)
83
);
84
85
const requestCtx = new RequestContext(
86
request,
87
ultraviolet,
88
!emptyMethods.includes(request.method.toUpperCase())
89
? await request.blob()
90
: null
91
);
92
93
if (ultraviolet.meta.url.protocol === 'blob:') {
94
requestCtx.blob = true;
95
requestCtx.base = requestCtx.url = new URL(requestCtx.url.pathname);
96
}
97
98
if (request.referrer && request.referrer.startsWith(location.origin)) {
99
const referer = new URL(ultraviolet.sourceUrl(request.referrer));
100
101
if (
102
requestCtx.headers.origin ||
103
(ultraviolet.meta.url.origin !== referer.origin &&
104
request.mode === 'cors')
105
) {
106
requestCtx.headers.origin = referer.origin;
107
}
108
109
requestCtx.headers.referer = referer.href;
110
}
111
112
const cookies = (await ultraviolet.cookie.getCookies(db)) || [];
113
const cookieStr = ultraviolet.cookie.serialize(
114
cookies,
115
ultraviolet.meta,
116
false
117
);
118
119
requestCtx.headers['user-agent'] = navigator.userAgent;
120
121
if (cookieStr) requestCtx.headers.cookie = cookieStr;
122
123
const reqEvent = new HookEvent(requestCtx, null, null);
124
this.emit('request', reqEvent);
125
126
if (reqEvent.intercepted) return reqEvent.returnValue;
127
128
fetchedURL = requestCtx.blob
129
? 'blob:' + location.origin + requestCtx.url.pathname
130
: requestCtx.url;
131
132
const response = await this.bareClient.fetch(fetchedURL, {
133
headers: requestCtx.headers,
134
method: requestCtx.method,
135
body: requestCtx.body,
136
credentials: requestCtx.credentials,
137
mode: requestCtx.mode,
138
cache: requestCtx.cache,
139
redirect: requestCtx.redirect,
140
});
141
142
const responseCtx = new ResponseContext(requestCtx, response);
143
const resEvent = new HookEvent(responseCtx, null, null);
144
145
this.emit('beforemod', resEvent);
146
if (resEvent.intercepted) return resEvent.returnValue;
147
148
for (const name of cspHeaders) {
149
if (responseCtx.headers[name]) delete responseCtx.headers[name];
150
}
151
152
if (responseCtx.headers.location) {
153
responseCtx.headers.location = ultraviolet.rewriteUrl(
154
responseCtx.headers.location
155
);
156
}
157
158
// downloads
159
if (['document', 'iframe'].includes(request.destination)) {
160
const header = responseCtx.getHeader('content-disposition');
161
162
// validate header and test for filename
163
if (!/\s*?((inline|attachment);\s*?)filename=/i.test(header)) {
164
// if filename= wasn't specified then maybe the remote specified to download this as an attachment?
165
// if it's invalid then we can still possibly test for the attachment/inline type
166
const type = /^\s*?attachment/i.test(header)
167
? 'attachment'
168
: 'inline';
169
170
// set the filename
171
const [filename] = new URL(response.finalURL).pathname
172
.split('/')
173
.slice(-1);
174
175
responseCtx.headers['content-disposition'] =
176
`${type}; filename=${JSON.stringify(filename)}`;
177
}
178
}
179
180
if (responseCtx.headers['set-cookie']) {
181
Promise.resolve(
182
ultraviolet.cookie.setCookies(
183
responseCtx.headers['set-cookie'],
184
db,
185
ultraviolet.meta
186
)
187
).then(() => {
188
self.clients.matchAll().then(function (clients) {
189
clients.forEach(function (client) {
190
client.postMessage({
191
msg: 'updateCookies',
192
url: ultraviolet.meta.url.href,
193
});
194
});
195
});
196
});
197
delete responseCtx.headers['set-cookie'];
198
}
199
200
if (responseCtx.body) {
201
switch (request.destination) {
202
case 'script':
203
responseCtx.body = ultraviolet.js.rewrite(await response.text());
204
break;
205
case 'worker':
206
{
207
// craft a JS-safe list of arguments
208
const scripts = [
209
ultraviolet.bundleScript,
210
ultraviolet.clientScript,
211
ultraviolet.configScript,
212
ultraviolet.handlerScript,
213
]
214
.map((script) => JSON.stringify(script))
215
.join(',');
216
responseCtx.body = `if (!self.__uv) {
217
${ultraviolet.createJsInject(
218
ultraviolet.cookie.serialize(
219
cookies,
220
ultraviolet.meta,
221
true
222
),
223
request.referrer
224
)}
225
importScripts(${scripts});
226
}\n`;
227
responseCtx.body += ultraviolet.js.rewrite(await response.text());
228
}
229
break;
230
case 'style':
231
responseCtx.body = ultraviolet.rewriteCSS(await response.text());
232
break;
233
case 'iframe':
234
case 'document':
235
if (
236
responseCtx.getHeader('content-type') &&
237
responseCtx.getHeader('content-type').startsWith('text/html')
238
) {
239
let modifiedResponse = await response.text();
240
if (Array.isArray(this.config.inject)) {
241
const headPosition = modifiedResponse.indexOf('<head>');
242
const upperHead = modifiedResponse.indexOf('<HEAD>');
243
const bodyPosition = modifiedResponse.indexOf('<body>');
244
const upperBody = modifiedResponse.indexOf('<BODY>');
245
const url = new URL(fetchedURL);
246
const injectArray = this.config.inject;
247
for (const inject of injectArray) {
248
const regex = new RegExp(inject.host);
249
if (regex.test(url.host)) {
250
if (inject.injectTo === 'head') {
251
if (headPosition !== -1 || upperHead !== -1) {
252
modifiedResponse =
253
modifiedResponse.slice(0, headPosition) +
254
`${inject.html}` +
255
modifiedResponse.slice(headPosition);
256
}
257
} else if (inject.injectTo === 'body') {
258
if (bodyPosition !== -1 || upperBody !== -1) {
259
modifiedResponse =
260
modifiedResponse.slice(0, bodyPosition) +
261
`${inject.html}` +
262
modifiedResponse.slice(bodyPosition);
263
}
264
}
265
}
266
}
267
}
268
responseCtx.body = ultraviolet.rewriteHtml(modifiedResponse, {
269
document: true,
270
injectHead: ultraviolet.createHtmlInject(
271
ultraviolet.handlerScript,
272
ultraviolet.bundleScript,
273
ultraviolet.clientScript,
274
ultraviolet.configScript,
275
ultraviolet.cookie.serialize(cookies, ultraviolet.meta, true),
276
request.referrer
277
),
278
});
279
}
280
break;
281
default:
282
break;
283
}
284
}
285
286
if (requestCtx.headers.accept === 'text/event-stream') {
287
responseCtx.headers['content-type'] = 'text/event-stream';
288
}
289
if (crossOriginIsolated) {
290
responseCtx.headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
291
}
292
293
this.emit('response', resEvent);
294
if (resEvent.intercepted) return resEvent.returnValue;
295
296
return new Response(responseCtx.body, {
297
headers: responseCtx.headers,
298
status: responseCtx.status,
299
statusText: responseCtx.statusText,
300
});
301
} catch (err) {
302
if (!['document', 'iframe'].includes(request.destination))
303
return new Response(undefined, { status: 500 });
304
305
console.error(err);
306
307
return renderError(err, fetchedURL);
308
}
309
}
310
static Ultraviolet = Ultraviolet;
311
}
312
313
self.UVServiceWorker = UVServiceWorker;
314
315
class ResponseContext {
316
/**
317
*
318
* @param {RequestContext} request
319
* @param {import('@mercuryworkshop/bare-mux').BareResponseFetch} response
320
*/
321
constructor(request, response) {
322
this.request = request;
323
this.raw = response;
324
this.ultraviolet = request.ultraviolet;
325
this.headers = {};
326
// eg set-cookie
327
for (const key in response.rawHeaders)
328
this.headers[key.toLowerCase()] = response.rawHeaders[key];
329
this.status = response.status;
330
this.statusText = response.statusText;
331
this.body = response.body;
332
}
333
get url() {
334
return this.request.url;
335
}
336
get base() {
337
return this.request.base;
338
}
339
set base(val) {
340
this.request.base = val;
341
}
342
//the header value might be an array, so this function is used to
343
//retrieve the value when it needs to be compared against a string
344
getHeader(key) {
345
if (Array.isArray(this.headers[key])) {
346
return this.headers[key][0];
347
}
348
return this.headers[key];
349
}
350
}
351
352
class RequestContext {
353
/**
354
*
355
* @param {Request} request
356
* @param {Ultraviolet} ultraviolet
357
* @param {BodyInit} body
358
*/
359
constructor(request, ultraviolet, body = null) {
360
this.ultraviolet = ultraviolet;
361
this.request = request;
362
this.headers = Object.fromEntries(request.headers.entries());
363
this.method = request.method;
364
this.body = body || null;
365
this.cache = request.cache;
366
this.redirect = request.redirect;
367
this.credentials = 'omit';
368
this.mode = request.mode === 'cors' ? request.mode : 'same-origin';
369
this.blob = false;
370
}
371
get url() {
372
return this.ultraviolet.meta.url;
373
}
374
set url(val) {
375
this.ultraviolet.meta.url = val;
376
}
377
get base() {
378
return this.ultraviolet.meta.base;
379
}
380
set base(val) {
381
this.ultraviolet.meta.base = val;
382
}
383
}
384
385
class HookEvent {
386
#intercepted;
387
#returnValue;
388
constructor(data = {}, target = null, that = null) {
389
this.#intercepted = false;
390
this.#returnValue = null;
391
this.data = data;
392
this.target = target;
393
this.that = that;
394
}
395
get intercepted() {
396
return this.#intercepted;
397
}
398
get returnValue() {
399
return this.#returnValue;
400
}
401
respondWith(input) {
402
this.#returnValue = input;
403
this.#intercepted = true;
404
}
405
}
406
407
/**
408
*
409
* @param {string} trace
410
* @param {string} fetchedURL
411
* @returns
412
*/
413
function errorTemplate(trace, fetchedURL) {
414
// turn script into a data URI so we don't have to escape any HTML values
415
const script = `
416
errorTrace.value = ${JSON.stringify(trace)};
417
fetchedURL.textContent = ${JSON.stringify(fetchedURL)};
418
for (const node of document.querySelectorAll('#uvHostname')) node.textContent = ${JSON.stringify(
419
location.hostname
420
)};
421
reload.addEventListener('click', () => location.reload());
422
uvVersion.textContent = ${JSON.stringify('3.2.7')};
423
`;
424
425
return '{{ultraviolet-error}}'.replace(
426
'{{src}}',
427
'data:application/javascript,' + encodeURIComponent(script)
428
);
429
}
430
431
/**
432
*
433
* @param {unknown} err
434
* @param {string} fetchedURL
435
*/
436
function renderError(err, fetchedURL) {
437
let headers = {
438
'content-type': 'text/html',
439
};
440
if (crossOriginIsolated) {
441
headers['Cross-Origin-Embedder-Policy'] = 'require-corp';
442
}
443
444
return new Response(errorTemplate(String(err), fetchedURL), {
445
status: 500,
446
headers: headers,
447
});
448
}
449
450