/*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} */ 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; } }