339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
/*globals __uv$config*/
|
|
// Users must import the config (and bundle) prior to importing uv.sw.js
|
|
// This is to allow us to produce a generic bundle with no hard-coded paths.
|
|
|
|
/**
|
|
* @typedef {typeof import("./rewrite/index.js").default} Ultraviolet
|
|
*/
|
|
|
|
/**
|
|
* @type {typeof import("./rewrite/index.js").default}
|
|
*/
|
|
const Ultraviolet = globalThis.Ultraviolet;
|
|
|
|
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',
|
|
];
|
|
const emptyMethods = ['GET', 'HEAD'];
|
|
|
|
class UVServiceWorker extends Ultraviolet.EventEmitter {
|
|
constructor(config = __uv$config) {
|
|
super();
|
|
if (!config.bare) config.bare = '/bare/';
|
|
if (!config.prefix) config.prefix = '/service/';
|
|
this.addresses =
|
|
typeof config.bare === 'string'
|
|
? [new URL(config.bare, location)]
|
|
: config.bare.map((str) => new URL(str, location));
|
|
this.config = config;
|
|
/**
|
|
* @type {InstanceType<Ultraviolet['BareClient']>}
|
|
*/
|
|
this.bareClient = new Ultraviolet.BareClient(this.address);
|
|
}
|
|
/**
|
|
*
|
|
* @param {Event & {request: Request}} param0
|
|
* @returns
|
|
*/
|
|
async fetch({ request }) {
|
|
try {
|
|
if (!request.url.startsWith(location.origin + this.config.prefix))
|
|
return await fetch(request);
|
|
|
|
const ultraviolet = new Ultraviolet(this.config);
|
|
|
|
if (typeof this.config.construct === 'function') {
|
|
this.config.construct(ultraviolet, 'service');
|
|
}
|
|
|
|
const db = await ultraviolet.cookie.db();
|
|
|
|
ultraviolet.meta.origin = location.origin;
|
|
ultraviolet.meta.base = ultraviolet.meta.url = new URL(
|
|
ultraviolet.sourceUrl(request.url)
|
|
);
|
|
|
|
const requestCtx = new RequestContext(
|
|
request,
|
|
this,
|
|
ultraviolet,
|
|
!emptyMethods.includes(request.method.toUpperCase())
|
|
? await request.blob()
|
|
: null
|
|
);
|
|
|
|
if (ultraviolet.meta.url.protocol === 'blob:') {
|
|
requestCtx.blob = true;
|
|
requestCtx.base = requestCtx.url = new URL(
|
|
requestCtx.url.pathname
|
|
);
|
|
}
|
|
|
|
if (
|
|
request.referrer &&
|
|
request.referrer.startsWith(location.origin)
|
|
) {
|
|
const referer = new URL(
|
|
ultraviolet.sourceUrl(request.referrer)
|
|
);
|
|
|
|
if (
|
|
requestCtx.headers.origin ||
|
|
(ultraviolet.meta.url.origin !== referer.origin &&
|
|
request.mode === 'cors')
|
|
) {
|
|
requestCtx.headers.origin = referer.origin;
|
|
}
|
|
|
|
requestCtx.headers.referer = referer.href;
|
|
}
|
|
|
|
const cookies = (await ultraviolet.cookie.getCookies(db)) || [];
|
|
const cookieStr = ultraviolet.cookie.serialize(
|
|
cookies,
|
|
ultraviolet.meta,
|
|
false
|
|
);
|
|
|
|
requestCtx.headers['user-agent'] = navigator.userAgent;
|
|
|
|
if (cookieStr) requestCtx.headers.cookie = cookieStr;
|
|
requestCtx.headers.Host = requestCtx.url.host;
|
|
|
|
const reqEvent = new HookEvent(requestCtx, null, null);
|
|
this.emit('request', reqEvent);
|
|
|
|
if (reqEvent.intercepted) return reqEvent.returnValue;
|
|
|
|
const response = await this.bareClient.fetch(
|
|
requestCtx.blob
|
|
? 'blob:' + location.origin + requestCtx.url.pathname
|
|
: requestCtx.url,
|
|
{
|
|
headers: requestCtx.headers,
|
|
method: requestCtx.method,
|
|
body: requestCtx.body,
|
|
credentials: requestCtx.credentials,
|
|
mode:
|
|
location.origin !== requestCtx.address.origin
|
|
? 'cors'
|
|
: requestCtx.mode,
|
|
redirect: requestCtx.redirect,
|
|
}
|
|
);
|
|
|
|
const responseCtx = new ResponseContext(requestCtx, response);
|
|
const resEvent = new HookEvent(responseCtx, null, null);
|
|
|
|
this.emit('beforemod', resEvent);
|
|
if (resEvent.intercepted) return resEvent.returnValue;
|
|
|
|
for (const name of cspHeaders) {
|
|
if (responseCtx.headers[name]) delete responseCtx.headers[name];
|
|
}
|
|
|
|
if (responseCtx.headers.location) {
|
|
responseCtx.headers.location = ultraviolet.rewriteUrl(
|
|
responseCtx.headers.location
|
|
);
|
|
}
|
|
|
|
if (responseCtx.headers['set-cookie']) {
|
|
Promise.resolve(
|
|
ultraviolet.cookie.setCookies(
|
|
responseCtx.headers['set-cookie'],
|
|
db,
|
|
ultraviolet.meta
|
|
)
|
|
).then(() => {
|
|
self.clients.matchAll().then(function (clients) {
|
|
clients.forEach(function (client) {
|
|
client.postMessage({
|
|
msg: 'updateCookies',
|
|
url: ultraviolet.meta.url.href,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
delete responseCtx.headers['set-cookie'];
|
|
}
|
|
|
|
if (responseCtx.body) {
|
|
switch (request.destination) {
|
|
case 'script':
|
|
case 'worker':
|
|
responseCtx.body = `if (!self.__uv && self.importScripts) importScripts('${ultraviolet.bundleScript}', '${ultraviolet.configScript}', '${ultraviolet.handlerScript}');\n`;
|
|
responseCtx.body += ultraviolet.js.rewrite(
|
|
await response.text()
|
|
);
|
|
break;
|
|
case 'style':
|
|
responseCtx.body = ultraviolet.rewriteCSS(
|
|
await response.text()
|
|
);
|
|
break;
|
|
case 'iframe':
|
|
case 'document':
|
|
if (
|
|
isHtml(
|
|
ultraviolet.meta.url,
|
|
responseCtx.headers['content-type'] || ''
|
|
)
|
|
) {
|
|
responseCtx.body = ultraviolet.rewriteHtml(
|
|
await response.text(),
|
|
{
|
|
document: true,
|
|
injectHead: ultraviolet.createHtmlInject(
|
|
ultraviolet.handlerScript,
|
|
ultraviolet.bundleScript,
|
|
ultraviolet.configScript,
|
|
this.bareClient.data,
|
|
ultraviolet.cookie.serialize(
|
|
cookies,
|
|
ultraviolet.meta,
|
|
true
|
|
),
|
|
request.referrer
|
|
),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (requestCtx.headers.accept === 'text/event-stream') {
|
|
responseCtx.headers['content-type'] = 'text/event-stream';
|
|
}
|
|
|
|
this.emit('response', resEvent);
|
|
if (resEvent.intercepted) return resEvent.returnValue;
|
|
|
|
return new Response(responseCtx.body, {
|
|
headers: responseCtx.headers,
|
|
status: responseCtx.status,
|
|
statusText: responseCtx.statusText,
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
return new Response(err.toString(), {
|
|
status: 500,
|
|
});
|
|
}
|
|
}
|
|
get address() {
|
|
return this.addresses[
|
|
Math.floor(Math.random() * this.addresses.length)
|
|
];
|
|
}
|
|
static Ultraviolet = Ultraviolet;
|
|
}
|
|
|
|
self.UVServiceWorker = UVServiceWorker;
|
|
|
|
class ResponseContext {
|
|
/**
|
|
*
|
|
* @param {RequestContext} request
|
|
* @param {import("@tomphttp/bare-client").BareResponseFetch} response
|
|
*/
|
|
constructor(request, response) {
|
|
this.request = request;
|
|
this.raw = response;
|
|
this.ultraviolet = request.ultraviolet;
|
|
this.headers = Object.fromEntries(response.headers.entries());
|
|
this.status = response.status;
|
|
this.statusText = response.statusText;
|
|
this.body = response.body;
|
|
}
|
|
get url() {
|
|
return this.request.url;
|
|
}
|
|
get base() {
|
|
return this.request.base;
|
|
}
|
|
set base(val) {
|
|
this.request.base = val;
|
|
}
|
|
}
|
|
|
|
class RequestContext {
|
|
/**
|
|
*
|
|
* @param {Request} request
|
|
* @param {UVServiceWorker} worker
|
|
* @param {Ultraviolet} ultraviolet
|
|
* @param {BodyInit} body
|
|
*/
|
|
constructor(request, worker, ultraviolet, body = null) {
|
|
this.ultraviolet = ultraviolet;
|
|
this.request = request;
|
|
this.headers = Object.fromEntries(request.headers.entries());
|
|
this.method = request.method;
|
|
this.address = worker.address;
|
|
this.body = body || null;
|
|
this.redirect = request.redirect;
|
|
this.credentials = 'omit';
|
|
this.mode = request.mode === 'cors' ? request.mode : 'same-origin';
|
|
this.blob = false;
|
|
}
|
|
get url() {
|
|
return this.ultraviolet.meta.url;
|
|
}
|
|
set url(val) {
|
|
this.ultraviolet.meta.url = val;
|
|
}
|
|
get base() {
|
|
return this.ultraviolet.meta.base;
|
|
}
|
|
set base(val) {
|
|
this.ultraviolet.meta.base = val;
|
|
}
|
|
}
|
|
|
|
function isHtml(url, contentType = '') {
|
|
return (
|
|
(
|
|
Ultraviolet.mime.contentType(contentType || url.pathname) ||
|
|
'text/html'
|
|
).split(';')[0] === 'text/html'
|
|
);
|
|
}
|
|
|
|
class HookEvent {
|
|
#intercepted;
|
|
#returnValue;
|
|
constructor(data = {}, target = null, that = null) {
|
|
this.#intercepted = false;
|
|
this.#returnValue = null;
|
|
this.data = data;
|
|
this.target = target;
|
|
this.that = that;
|
|
}
|
|
get intercepted() {
|
|
return this.#intercepted;
|
|
}
|
|
get returnValue() {
|
|
return this.#returnValue;
|
|
}
|
|
respondWith(input) {
|
|
this.#returnValue = input;
|
|
this.#intercepted = true;
|
|
}
|
|
}
|