@specprotected/spec-proxy-service-worker
Version:
Server Worker API implementation for integrating with Spec Proxy from an Edge Worker
332 lines • 13.9 kB
JavaScript
;
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