UNPKG

@specprotected/spec-proxy-service-worker

Version:

Server Worker API implementation for integrating with Spec Proxy from an Edge Worker

332 lines 13.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RequestType = void 0; exports.specProxyProcessRequest = specProxyProcessRequest; exports.specProxyProcessResponse = specProxyProcessResponse; exports.specProxyProcess = specProxyProcess; exports.specMakeRequestWithFallback = specMakeRequestWithFallback; const cookie_1 = require("cookie"); const SPEC_PATH_PREFIX = "/spec_traffic"; const SPEC_HEADER_FORWARD_ORIGIN = "x-spec-forward-origin"; const SPEC_COOKIE_ID = "x-spec-id"; const SPEC_HEADER_CUSTOMER_KEY = "x-spec-customer-authorization"; const HEADER_SET_COOKIE = "set-cookie"; const HEADER_COOKIE = "cookie"; const HEADER_HOST = "host"; const HEADER_X_FORWARDED_FOR = "x-forwarded-for"; const HEADER_SPEC_ACTIVITY = "x-atvak-activity-count"; const SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH = "x-spec-req-prop-content-length"; const SPEC_MIRROR_HEADER_REQ_METHOD = "x-spec-req-prop-method"; const SPEC_MIRROR_HEADER_REQ_URI = "x-spec-req-prop-uri"; const SPEC_MIRROR_HEADER_RESP_STATUS = "x-spec-resp-prop-status"; const SPEC_MIRROR_HEADER_RESP_PREFIX = "x-spec-resp-head"; var RequestType; (function (RequestType) { RequestType[RequestType["Inline"] = 0] = "Inline"; RequestType[RequestType["Origin"] = 1] = "Origin"; RequestType[RequestType["Mirrored"] = 2] = "Mirrored"; })(RequestType || (exports.RequestType = RequestType = {})); function specProxyProcessRequest(event, config = {}, requestConstructor = Request) { const originalUrl = new URL(event.request.url); if (config.disableSpecProxy || !shouldHandleRequest(new URL(event.request.url), event.request.headers, config)) { return { specUrl: originalUrl, originalUrl, request: event.request, requestBodyForResponseProcessing: null, }; } if (config.debug) { console.log("specProxyProcessRequest: enabled"); } const request = event.request; const specUrl = makeSpecUrl(originalUrl, config, MessageType.Request); if (config.debug) { console.log(`specProxyProcessRequest: specUrl: ${specUrl}`); } const specHeaders = new Headers(request.headers); setCommonSpecHeaders(specHeaders, config); if (config.inlineMode || (!config.disableSpecTraffic && isSpecTraffic(originalUrl))) { if (config.debug) { console.log(`specProxyProcessRequest: inline`); } setForwardingSpecHeaders(specHeaders, originalUrl, config); return { specUrl, originalUrl, requestBodyForResponseProcessing: null, request: new requestConstructor(specUrl.toString(), { body: request.body, headers: specHeaders, method: request.method, redirect: request.redirect, }, request, config, RequestType.Inline), }; } else { if (config.debug) { console.log(`specProxyProcessRequest: mirror`); } let requestBody = request.body; if (config.splitExchangeMirroring) { if (config.debug) { console.log(`specProxyProcessRequest: split mirroring`); } const [specRequest, teedBody] = makeMirrorRequestSplit(specUrl, specHeaders, request, requestConstructor, config); if (teedBody) { requestBody = teedBody; } if (config.debug) { console.log(`specProxyProcessRequest: sending split mirror request: ${specRequest.url}`); } event.waitUntil(fetch(specRequest)); } let requestBodyForResponseProcessing = null; if (requestBody) { [requestBody, requestBodyForResponseProcessing] = requestBody.tee(); } return { specUrl, originalUrl, requestBodyForResponseProcessing, request: new requestConstructor(request.url, { body: requestBody, headers: request.headers, method: request.method, redirect: request.redirect, }, request, config, RequestType.Origin), }; } } function specProxyProcessResponse(event, req, response, config = {}, requestConstructor = Request) { const request = req.request; const url = new URL(request.url); if (config.disableSpecProxy || config.inlineMode === true || isSpecTraffic(url) || !shouldHandleRequest(url, request.headers, config)) { return response; } if (config.debug) { console.log(`specProxyProcessResponse: enabled`); } let cookies = {}; if (request.headers) { cookies = (0, cookie_1.parse)(request.headers.get(HEADER_COOKIE) || ""); } if (!cookies[SPEC_COOKIE_ID]) { let domain = extractTopLevelDomain(request.headers) || undefined; if (config.localDev && (domain === null || domain === void 0 ? void 0 : domain.startsWith("localhost:"))) { domain = domain.split(":")[0]; } let specId = crypto.randomUUID(); let setCookie = (0, cookie_1.serialize)(SPEC_COOKIE_ID, specId, { maxAge: 320000000, domain, path: "/", sameSite: "none", secure: true, }); response = new Response(response.body, response); response.headers.append(HEADER_SET_COOKIE, setCookie); } const [mirrorRequest, responseBody] = makeMirrorExchangeRequest(req, response, config, requestConstructor); if (config.debug) { console.log(`specProxyProcessResponse: mirror exchange request: ` + `${mirrorRequest.url}: ${mirrorRequest.headers.get(SPEC_MIRROR_HEADER_REQ_URI)}` + `: ${mirrorRequest.headers.get("host")}`); } event.waitUntil(fetch(mirrorRequest).then((resp) => { if (config.debug) { console.log(`specProxyProcessResponse: response from mirror server: ${resp.status}: ${resp.statusText}`); } })); response = new Response(responseBody, response); return response; } async function specProxyProcess(event, config, requestConstructor) { let request = specProxyProcessRequest(event, config, requestConstructor); if (config.debug) { console.log(`specProxyProcess: sending request to downstream: ${request.request.url}`); } let response = await specMakeRequestWithFallback(event, request.request, config, requestConstructor); return specProxyProcessResponse(event, request, response, config); } async function specMakeRequestWithFallback(event, request, config, requestConstructor) { if (config.inlineMode) { let [initialBody, fallbackBody] = [null, null]; if (event.request.body) { [initialBody, fallbackBody] = event.request.body.tee(); } let response = await fetch(new requestConstructor(request, { body: initialBody, })); if (response.headers.get(HEADER_SPEC_ACTIVITY)) { fallbackBody === null || fallbackBody === void 0 ? void 0 : fallbackBody.cancel(); let newResponse = new Response(response.body, response); newResponse.headers.delete(HEADER_SPEC_ACTIVITY); return newResponse; } if (config.debug) { console.log("specMakeReqeustWithFallback: making fallback request"); } return await fetch(new requestConstructor(event.request, { body: fallbackBody })); } else { return await fetch(request); } } function shouldHandleRequest(url, headers, config) { if (!config.disableSpecTraffic && url.pathname.startsWith(SPEC_PATH_PREFIX)) { return true; } if (config.percentageOfIPs === undefined || config.percentageOfIPs >= 100) { return true; } else if (config.percentageOfIPs <= 0) { return false; } let ip_octet_sum = (headers.get(HEADER_X_FORWARDED_FOR) || "99") .split(".") .map((octet) => parseInt(octet)) .reduce((acc, n) => acc + n); if (isNaN(ip_octet_sum)) { ip_octet_sum = 99; } return ip_octet_sum % 100 < config.percentageOfIPs; } function extractTopLevelDomain(headers) { var _a; let host = headers.get(HEADER_HOST); if (host) { const domain_extract = /^(.*?\.)?(?<domain>[^.]+\.(com|co|org|edu|net|int|gov|mil|uk|co\.uk|ac\.uk|gov\.uk|ltd\.uk|me\.uk|net\.uk|nhs\.uk|org\.uk|plc\.uk|police\.uk))(\.spec-internal\.com)?$/; let matches = domain_extract.exec(host); if ((_a = matches === null || matches === void 0 ? void 0 : matches.groups) === null || _a === void 0 ? void 0 : _a.domain) { host = matches.groups.domain; } } return host; } var MessageType; (function (MessageType) { MessageType[MessageType["Request"] = 0] = "Request"; MessageType[MessageType["Response"] = 1] = "Response"; })(MessageType || (MessageType = {})); function makeSpecUrl(originalUrl, config, messageType) { const originalHost = originalUrl.hostname; const specUrl = new URL(originalUrl); specUrl.protocol = config.localDev ? "http" : "https"; specUrl.hostname = config.localDev ? "localhost" : specUrl.hostname.endsWith(".spec-internal.com") ? specUrl.hostname : config.domainOverride ? `${config.domainOverride}.spec-internal.com` : `${originalHost}.spec-internal.com`; if (config.localDev) { specUrl.port = "5000"; } setSpecUrlPath(specUrl, config, messageType); return specUrl; } function setSpecUrlPath(specUrl, config, messageType) { if (config.inlineMode || isSpecTraffic(specUrl)) { return; } if (!config.splitExchangeMirroring) { specUrl.pathname = "/speclayer/api/mirror/exchanges"; } else if (messageType === MessageType.Request) { specUrl.pathname = "/speclayer/api/mirror/requests"; } else if (messageType === MessageType.Response) { specUrl.pathname = "/speclayer/api/mirror/responses"; } specUrl.search = ""; } function isSpecTraffic(url) { return url.pathname.startsWith(SPEC_PATH_PREFIX); } function setCommonSpecHeaders(specHeaders, config) { specHeaders.set("x-spec-origination-ts", new Date().toISOString()); if (config.customerKey) { specHeaders.set(SPEC_HEADER_CUSTOMER_KEY, config.customerKey); } } function setForwardingSpecHeaders(specHeaders, originalUrl, config) { if (config.localDev && originalUrl.hostname === "localhost") { specHeaders.set(SPEC_HEADER_FORWARD_ORIGIN, `http://${originalUrl.host}`); } else { specHeaders.set(SPEC_HEADER_FORWARD_ORIGIN, originalUrl.hostname); } } function setMirrorRequestHeaders(specHeaders, originalRequest) { const contentLength = originalRequest.headers.get("content-length"); if (contentLength) { specHeaders.set(SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH, contentLength); } specHeaders.set(SPEC_MIRROR_HEADER_REQ_METHOD, originalRequest.method); specHeaders.set(SPEC_MIRROR_HEADER_REQ_URI, originalRequest.url); } function setMirrorResponseHeaders(specHeaders, originalResponse) { specHeaders.set(SPEC_MIRROR_HEADER_RESP_STATUS, originalResponse.status.toString()); for (const key of specHeaders.keys()) { if (key.startsWith(SPEC_MIRROR_HEADER_RESP_PREFIX)) { specHeaders.delete(key); } } originalResponse.headers.forEach((val, name, _headers) => { specHeaders.append(`${SPEC_MIRROR_HEADER_RESP_PREFIX}-${name}`, val); }); } function makeMirrorRequestSplit(specUrl, specHeaders, originalRequest, requestConstructor, config) { setMirrorRequestHeaders(specHeaders, originalRequest); let teedBody = [null, null]; if (originalRequest.body !== null && originalRequest.body !== undefined) { teedBody = originalRequest.body.tee(); } const specRequest = new requestConstructor(specUrl.toString(), { body: teedBody[0], headers: specHeaders, method: "POST", redirect: originalRequest.redirect, }, originalRequest, config, RequestType.Mirrored); return [specRequest, teedBody[1]]; } function makeMirrorExchangeRequest(req, originalResponse, config, requestConstructor) { const originalRequest = req.request; const specHeaders = new Headers(originalRequest.headers); setCommonSpecHeaders(specHeaders, config); setMirrorRequestHeaders(specHeaders, originalRequest); setMirrorResponseHeaders(specHeaders, originalResponse); const [originalResponseBody, requestBody] = makeMirrorExchangeBodyUnified(req, originalResponse); const specUrl = makeSpecUrl(req.originalUrl, config, MessageType.Response); return [ new requestConstructor(specUrl.toString(), { body: requestBody, headers: specHeaders, method: "POST", }, originalRequest, config, RequestType.Mirrored), originalResponseBody, ]; } const DIVIDER = new Blob([ "-------SPEC-DIVIDER------- Wir betreten feuertrunken Himmlische, dein Heiligtum -------SPEC-DIVIDER-------\n", ]); function makeMirrorExchangeBodyUnified(req, originalResponse) { const [originalResponseBody, teedResponseBody] = originalResponse.body ? originalResponse.body.tee() : [originalResponse.body, null]; const components = [ req.requestBodyForResponseProcessing || new Blob([]).stream(), DIVIDER.stream(), teedResponseBody || new Blob([]).stream(), ]; const { readable: mirrorExchangeStream, writable } = new TransformStream(); components.reduce((acc, i, idx, arr) => acc.then(() => i.pipeTo(writable, { preventClose: idx + 1 !== arr.length })), Promise.resolve()); return [originalResponseBody, mirrorExchangeStream]; } //# sourceMappingURL=index.js.map