@specprotected/spec-proxy-service-worker
Version:
Server Worker API implementation for integrating with Spec Proxy from an Edge Worker
679 lines (621 loc) • 23.1 kB
text/typescript
// Note that for the most part, everything has to be mocked to the
// gills for tests here, so we do most of our testing in a separate
// integration suite and predominantly test purer private functions
// here.
import {
jest,
describe,
beforeEach,
test,
expect,
it,
beforeAll,
} from "@jest/globals";
import rewire from "rewire";
import makeServiceWorkerEnv from "service-worker-mock";
import { parse as parseCookies } from "cookie";
import * as buffer from "node:buffer";
import * as stream from "node:stream/web";
import * as node_util from "node:util";
import {
specProxyProcessRequest,
specProxyProcessResponse,
specMakeRequestWithFallback,
} from "./index";
// Import the header constant for testing
const HEADER_SPEC_ACTIVITY = "x-atvak-activity-count";
// Workers supply `crypto` globally, but it's not present in our testing environment
// so let's fake it.
import * as crypto from "crypto";
Object.defineProperty(globalThis, "crypto", {
value: {
randomUUID: () => crypto.randomUUID(),
},
});
const TEST_URL = new URL("https://specprotected.com");
const TEST_SPEC_TRAFFIC_URL = new URL(
"https://specprotected.com/spec_traffic/somewhere",
);
describe("processing", () => {
beforeEach(() => {
// temporary workaround until https://github.com/nodejs/node/pull/46615#issuecomment-1604484636
// lands in node 18.x
delete global.location;
delete global.performance;
delete global.navigator;
// delete global.self;
Object.assign(global, makeServiceWorkerEnv());
Object.assign(globalThis, {
Blob: buffer.Blob,
File: buffer.File,
TextDecoder: node_util.TextDecoder,
TextEncoder: node_util.TextEncoder,
ReadableStream: stream.ReadableStream,
TransformStream: stream.TransformStream,
WritableStream: stream.WritableStream,
});
// blow away the Promise from Fetch API
global.fetch = (v: any) => v;
// create a fake version of the ExtendableEvent to use,
// also doesn't use Promises
global.simple_event = {
waitUntil: jest.fn((v: any) => v),
request: new Request("https://spectrust.com", {
method: "GET",
headers: {
"X-Forwarded-For": "127.0.0.1",
Host: "spectrust.com",
},
// ReadableStream is difficult to mock so can't test mirroring
// which modifies the body object...
// we also have to set it to null because service-worker-mock uses
// a garbage implementation of this when it's not provided and disrupts
// our use of the tee() function
body: null,
}),
};
});
test("disabled proxy is noop", () => {
expect(
specProxyProcessRequest(global.simple_event, { disableSpecProxy: true })
.request,
).toBe(global.simple_event.request);
expect(
specProxyProcessRequest(global.simple_event, { disableSpecProxy: false })
.request,
).not.toBe(global.simple_event.request);
});
test("traffic split", () => {
expect(
specProxyProcessRequest(global.simple_event, { percentageOfIPs: 0 })
.request,
).toBe(global.simple_event.request);
expect(
specProxyProcessRequest(global.simple_event, { percentageOfIPs: 100 })
.request,
).not.toBe(global.simple_event.request);
});
test("proxy forks request in mirror split mode", () => {
expect(
specProxyProcessRequest(global.simple_event, {
splitExchangeMirroring: true,
}).request,
).toEqual(global.simple_event.request);
expect(global.simple_event.waitUntil.mock.calls.length).toEqual(1);
});
// Note: libraries that make the request will change the Host header
// for us. Cloudflare does this automatically, Fastly does this through
// the use of the `backend` configuration property on the request.
test("in mirror split mode, Host header of mirrored request is unchanged", () => {
expect(
specProxyProcessRequest(global.simple_event, {
splitExchangeMirroring: true,
}).request,
).toEqual(global.simple_event.request);
let specProxyRequest = global.simple_event.waitUntil.mock.calls[0][0];
console.log(specProxyRequest.headers, global.simple_event.request.headers);
expect(specProxyRequest.headers.get("host")).toEqual(
global.simple_event.request.headers.get("host"),
);
});
test("proxy replaces request in inline mode", () => {
let request = specProxyProcessRequest(global.simple_event, {
inlineMode: true,
}).request;
expect(request).not.toEqual(global.simple_event.request);
expect(request.url).toEqual("https://spectrust.com.spec-internal.com/");
expect(global.simple_event.waitUntil.mock.calls.length).toEqual(0);
});
test("sending traffic to /spec_traffic in inline mode goes to spec proxy", () => {
let event = global.simple_event;
event.request.url = "https://spectrust.com/spec_traffic/somewhere";
let request = specProxyProcessRequest(global.simple_event, {
inlineMode: true,
}).request;
expect(request.url).toEqual(
"https://spectrust.com.spec-internal.com/spec_traffic/somewhere",
);
expect(global.simple_event.waitUntil.mock.calls.length).toEqual(0);
});
test("sending traffic to /spec_traffic in mirror mode goes to spec proxy", () => {
let event = global.simple_event;
event.request.url = "https://spectrust.com/spec_traffic/somewhere";
let request = specProxyProcessRequest(global.simple_event, {
inlineMode: false,
}).request;
expect(request.url).toEqual(
"https://spectrust.com.spec-internal.com/spec_traffic/somewhere",
);
expect(global.simple_event.waitUntil.mock.calls.length).toEqual(0);
});
test("sending traffic to /spec_traffic when it's disabled forwards traffic in mirror mode", () => {
let event = global.simple_event;
event.request.url = "https://spectrust.com/spec_traffic/somewhere";
let request = specProxyProcessRequest(global.simple_event, {
disableSpecTraffic: true,
splitExchangeMirroring: true,
}).request;
expect(request).toEqual(global.simple_event.request);
expect(global.simple_event.waitUntil.mock.calls.length).toEqual(1);
});
});
//---------------------------------------------
// Private Function Testing
//---------------------------------------------
// "rewire" the library module to allow us to reach private functions
let spec = rewire("./dist/index.js");
describe("shouldHandleRequest", () => {
let shouldHandleRequest = spec.__get__("shouldHandleRequest");
test.each([
["0.0.0.0", undefined, true],
["0.0.0.40", undefined, true],
["0.0.0.99", undefined, true],
["0.0.0.100", undefined, true], // "wraps" to 0
["0.0.0.0", 100, true],
["0.0.0.40", 100, true],
["0.0.0.99", 100, true],
["0.0.0.0", 100, true],
["0.0.0.40", 50, true],
["0.0.0.49", 50, true],
["0.0.0.50", 50, false],
["0.0.0.60", 50, false],
["0.0.0.0", 0, false],
["0.0.0.40", 0, false],
["0.0.0.99", 0, false],
["0.0.0.100", 0, false],
// some legit-looking IPs that don't exploit our algorithm for testing
["24.68.195.11", 50, false],
["74.232.255.255", 50, true],
["89.2.79.2", 50, false],
["67.67.67.67", 50, false],
["10.0.0.8", 50, true],
])("traffic split [ip=%s, split=%i]", (ip, split, expected) => {
let headers = new Headers({ "x-forwarded-for": ip });
expect(
shouldHandleRequest(TEST_URL, headers, {
percentageOfIPs: split,
}),
).toEqual(expected);
});
test.each([
["0.0.0.100", undefined, true], // "wraps" to 0
["0.0.0.99", 100, true],
["0.0.0.0", 100, true],
["0.0.0.49", 50, true],
["0.0.0.50", 50, true],
["0.0.0.60", 50, true],
["0.0.0.0", 0, true],
["0.0.0.40", 0, true],
["0.0.0.99", 0, true],
["0.0.0.100", 0, true],
// some legit-looking IPs that don't exploit our algorithm for testing
["24.68.195.11", 50, true],
["74.232.255.255", 50, true],
["89.2.79.2", 50, true],
["67.67.67.67", 50, true],
["10.0.0.8", 50, true],
])(
"traffic split [ip=%s, split=%i], always send to /spec_traffic when enabled",
(ip, split, expected) => {
let headers = new Headers({ "x-forwarded-for": ip });
expect(
shouldHandleRequest(TEST_SPEC_TRAFFIC_URL, headers, {
percentageOfIPs: split,
}),
).toEqual(expected);
},
);
test.each([
["0.0.0.100", undefined, true], // "wraps" to 0
["0.0.0.99", 100, true],
["0.0.0.0", 100, true],
["0.0.0.49", 50, true],
["0.0.0.50", 50, false],
["0.0.0.60", 50, false],
["0.0.0.0", 0, false],
["0.0.0.40", 0, false],
["0.0.0.99", 0, false],
["0.0.0.100", 0, false],
// some legit-looking IPs that don't exploit our algorithm for testing
["24.68.195.11", 50, false],
["74.232.255.255", 50, true],
["89.2.79.2", 50, false],
["67.67.67.67", 50, false],
["10.0.0.8", 50, true],
])(
"traffic split [ip=%s, split=%i], normal behavior when /spec_traffic disabled",
(ip, split, expected) => {
let headers = new Headers({ "x-forwarded-for": ip });
expect(
shouldHandleRequest(TEST_SPEC_TRAFFIC_URL, headers, {
percentageOfIPs: split,
disableSpecTraffic: true,
}),
).toEqual(expected);
},
);
test.each([
["X-Forwarded-For", 50, "0.0.0.0", true],
["X-Forwarded-For", 50, "0.0.0.60", false],
["Not-Checked-Header", 50, "0.0.0.0", false],
["Not-Checked-Header", 50, "0.0.0.99", false],
// if it's 100% split, we expect to handle the request even if we can't find the
// IP address on this header
["X-Forwarded-For", 100, "0.0.0.60", true],
["Not-Checked-Header", 100, "0.0.0.99", true],
])(
'ip from header "%s" with %i%% split for ip %s"',
(header, split, ip, expected) => {
let headers = new Headers({});
headers.set(header, ip);
expect(
shouldHandleRequest(TEST_URL, headers, {
percentageOfIPs: split,
}),
).toEqual(expected);
},
);
// Check NaN handling, just in case.
test.each([
["X-Forwarded-For", 50, "wtf", false],
["X-Forwarded-For", 99, "srs", false],
// if it's 100% split, we expect to handle the request even if we can't find the
// IP address on this header
["X-Forwarded-For", 100, "susfam", true],
["X-Forwarded-For", 100, ":ghost-shrug:", true],
])(
'ip from header "%s" with %i%% split for ip %s"',
(header, split, ip, expected) => {
let headers = new Headers({});
headers.set(header, ip);
expect(
shouldHandleRequest(TEST_URL, headers, {
percentageOfIPs: split,
}),
).toEqual(expected);
},
);
});
describe("extractTopLevelDomain", () => {
let extractTopLevelDomain = spec.__get__("extractTopLevelDomain");
test.each([
["spec-trust.com", "spec-trust.com"],
["somewhere.spec-trust.com", "spec-trust.com"],
["somewhere.spec-trust.com.spec-internal.com", "spec-trust.com"],
["somewhere.spec-trust.edu", "spec-trust.edu"],
["somewhere.spec-trust.edu.spec-internal.com", "spec-trust.edu"],
["somewhere.spec-trust.gov", "spec-trust.gov"],
["somewhere.spec-trust.gov.spec-internal.com", "spec-trust.gov"],
["somewhere.spec-trust.int", "spec-trust.int"],
["somewhere.spec-trust.int.spec-internal.com", "spec-trust.int"],
["somewhere.spec-trust.net", "spec-trust.net"],
["somewhere.spec-trust.net.spec-internal.com", "spec-trust.net"],
["somewhere.spec-trust.org", "spec-trust.org"],
["somewhere.spec-trust.org.spec-internal.com", "spec-trust.org"],
["somewhere.spec-trust.uk", "spec-trust.uk"],
["somewhere.spec-trust.uk.spec-internal.com", "spec-trust.uk"],
["many.many.many.many.sub-domains.spec-trust.com", "spec-trust.com"],
["any.suffixes", "any.suffixes"],
// if we don't match any known top-level domain suffix, we just get the whole thing
[
"subdomains.irrelevant.any.suffixes",
"subdomains.irrelevant.any.suffixes",
],
["invalid-top-level-though", "invalid-top-level-though"],
// invalid top-level domain suffix, but with spec-internal on the end winds up
// matching to spec-internal
["somewhere.fun.to.go.spec-internal.com", "spec-internal.com"],
["staging.special.somebody.co.uk.spec-internal.com", "somebody.co.uk"],
["staging.somebody.ac.uk.spec-internal.com", "somebody.ac.uk"],
["staging.somebody.co.uk.spec-internal.com", "somebody.co.uk"],
["staging.somebody.gov.uk.spec-internal.com", "somebody.gov.uk"],
["staging.somebody.ltd.uk.spec-internal.com", "somebody.ltd.uk"],
["staging.somebody.me.uk.spec-internal.com", "somebody.me.uk"],
["staging.somebody.net.uk.spec-internal.com", "somebody.net.uk"],
["staging.somebody.nhs.uk.spec-internal.com", "somebody.nhs.uk"],
["staging.somebody.plc.uk.spec-internal.com", "somebody.plc.uk"],
["staging.somebody.police.uk.spec-internal.com", "somebody.police.uk"],
])("extract top level domain from %s", (host, expected) => {
let headers = new Headers({ Host: host });
expect(extractTopLevelDomain(headers)).toEqual(expected);
});
});
describe("makeSpecUrl", () => {
let makeSpecUrl = spec.__get__("makeSpecUrl");
let MessageType = spec.__get__("MessageType");
test.each([
[
"inline mode redirects to spec-internal w/unchanged path & query string",
"https://www.customer.com/foobar?hello=world",
MessageType.Request,
{ inlineMode: true },
"https://www.customer.com.spec-internal.com/foobar?hello=world",
],
[
"default config (mirror mode) sends request to exchange endpoint",
"https://www.customer.com/foobar?hello=world",
MessageType.Request,
{},
"https://www.customer.com.spec-internal.com/speclayer/api/mirror/exchanges",
],
[
"default config (mirror mode) sends response to exchange endpoint",
"https://www.customer.com/foobar?hello=world",
MessageType.Response,
{},
"https://www.customer.com.spec-internal.com/speclayer/api/mirror/exchanges",
],
[
"split processing (mirror mode) sends request to request endpoint",
"https://www.customer.com/foobar?hello=world",
MessageType.Request,
{ splitExchangeMirroring: true },
"https://www.customer.com.spec-internal.com/speclayer/api/mirror/requests",
],
[
"split processing (mirror mode) sends response to response endpoint",
"https://www.customer.com/foobar?hello=world",
MessageType.Response,
{ splitExchangeMirroring: true },
"https://www.customer.com.spec-internal.com/speclayer/api/mirror/responses",
],
[
"domainOverride is applied if present",
"https://www.customer.com/foobar?hello=world",
MessageType.Request,
{ domainOverride: "alternative.com" },
"https://alternative.com.spec-internal.com/speclayer/api/mirror/exchanges",
],
[
"localDev always directs to localhost:5000",
"https://www.customer.com/foobar?hello=world",
MessageType.Request,
{ localDev: true },
"http://localhost:5000/speclayer/api/mirror/exchanges",
],
[
"we don't gaf about incoming protocol, it's always https",
"http://www.customer.com/foobar?hello=world",
MessageType.Request,
{},
"https://www.customer.com.spec-internal.com/speclayer/api/mirror/exchanges",
],
[
"we don't alter the path for spec-traffic",
"http://www.customer.com/spec_traffic/foobar?hello=world",
MessageType.Request,
{},
"https://www.customer.com.spec-internal.com/spec_traffic/foobar?hello=world",
],
[
"we don't change the domain if it already points to us",
"http://www.customer.com.spec-internal.com/spec_traffic/foobar?hello=world",
MessageType.Request,
{},
"https://www.customer.com.spec-internal.com/spec_traffic/foobar?hello=world",
],
])("%s", (_caseName, url, messageType, config, exp) => {
let specUrl = makeSpecUrl(new URL(url), config, messageType);
expect(specUrl.toString()).toEqual(exp);
});
});
describe("setCommonSpecHeaders", () => {
let setCommonSpecHeaders = spec.__get__("setCommonSpecHeaders");
let setForwardingSpecHeaders = spec.__get__("setForwardingSpecHeaders");
let setMirrorRequestHeaders = spec.__get__("setMirrorRequestHeaders");
let setMirrorResponseHeaders = spec.__get__("setMirrorResponseHeaders");
let SPEC_HEADER_CUSTOMER_KEY = spec.__get__("SPEC_HEADER_CUSTOMER_KEY");
const FIXED_TS = "2026-04-30T00:00:00.000Z";
beforeAll(() => {
const FixedDate = class extends Date {
toISOString() {
return FIXED_TS;
}
};
spec.__set__("Date", FixedDate);
});
afterAll(() => {
spec.__set__("Date", Date);
});
test.each([
[
"sets customer key if present",
new Headers(),
{ customerKey: "s3cr37" },
new Headers({
[SPEC_HEADER_CUSTOMER_KEY]: "s3cr37",
"x-spec-origination-ts": FIXED_TS,
}),
],
[
"overrides any existing customer key if present",
new Headers({ [SPEC_HEADER_CUSTOMER_KEY]: "f4k3rz" }),
{ customerKey: "s3cr37" },
new Headers({
[SPEC_HEADER_CUSTOMER_KEY]: "s3cr37",
"x-spec-origination-ts": FIXED_TS,
}),
],
[
"does nothing if no customer key present",
new Headers(),
{},
new Headers({ "x-spec-origination-ts": FIXED_TS }),
],
])("%s", (_caseName, headers, config, expHeaders) => {
setCommonSpecHeaders(headers, config);
// can't use direct equality, can't convert to object, but this works.
expect([...headers.entries()]).toEqual([...expHeaders.entries()]);
});
});
describe("setForwardingSpecHeaders", () => {
let setForwardingSpecHeaders = spec.__get__("setForwardingSpecHeaders");
let SPEC_HEADER_FORWARD_ORIGIN = spec.__get__("SPEC_HEADER_FORWARD_ORIGIN");
test.each([
[
"sets fwd-origin header to URL hostname",
new Headers(),
"https://customer.com/hewooo",
{},
new Headers({ [SPEC_HEADER_FORWARD_ORIGIN]: "customer.com" }),
],
[
"overrides fwd-origin header if present",
new Headers({ [SPEC_HEADER_FORWARD_ORIGIN]: "fakerz.com" }),
"https://customer.com/hewooo",
{},
new Headers({ [SPEC_HEADER_FORWARD_ORIGIN]: "customer.com" }),
],
[
"ignores port if present in URL",
new Headers(),
"https://customer.com:5000/hewooo",
{},
new Headers({ [SPEC_HEADER_FORWARD_ORIGIN]: "customer.com" }),
],
[
"includes http & port for localhost and localdev",
new Headers(),
"https://localhost:5000/hewooo",
{ localDev: true },
new Headers({ [SPEC_HEADER_FORWARD_ORIGIN]: "http://localhost:5000" }),
],
])(`makeSpecUrl: %s`, (_caseName, headers, url, config, expHeaders) => {
setForwardingSpecHeaders(headers, new URL(url), config);
expect([...headers.entries()]).toEqual([...expHeaders.entries()]);
});
});
describe("setMirrorRequestHeaders", () => {
let setMirrorRequestHeaders = spec.__get__("setMirrorRequestHeaders");
let SPEC_MIRROR_HEADER_REQ_METHOD = spec.__get__(
"SPEC_MIRROR_HEADER_REQ_METHOD",
);
let SPEC_MIRROR_HEADER_REQ_URI = spec.__get__("SPEC_MIRROR_HEADER_REQ_URI");
let SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH = spec.__get__(
"SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH",
);
// Allow us to make a Request
Object.assign(global, makeServiceWorkerEnv());
test.each([
[
"sets request method & uri",
new Headers(),
new Request("https://www.customer.com/foo?hello=world"),
new Headers({
[SPEC_MIRROR_HEADER_REQ_METHOD]: "GET",
[SPEC_MIRROR_HEADER_REQ_URI]:
"https://www.customer.com/foo?hello=world",
}),
],
[
"sets content length if present ",
new Headers(),
new Request("https://www.customer.com/foo?hello=world", {
headers: { "content-length": "50" },
}),
new Headers({
[SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH]: "50",
[SPEC_MIRROR_HEADER_REQ_METHOD]: "GET",
[SPEC_MIRROR_HEADER_REQ_URI]:
"https://www.customer.com/foo?hello=world",
}),
],
[
"overrides all headers if already present",
new Headers({
[SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH]: "0",
[SPEC_MIRROR_HEADER_REQ_METHOD]: "POST",
[SPEC_MIRROR_HEADER_REQ_URI]: "https://www.fakerz.com/foo?hello=world",
}),
new Request("https://www.customer.com/foo?hello=world", {
headers: { "content-length": "50" },
}),
new Headers({
[SPEC_MIRROR_HEADER_REQ_CONTENT_LENGTH]: "50",
[SPEC_MIRROR_HEADER_REQ_METHOD]: "GET",
[SPEC_MIRROR_HEADER_REQ_URI]:
"https://www.customer.com/foo?hello=world",
}),
],
])("%s", (_caseName, headers, req, expHeaders) => {
setMirrorRequestHeaders(headers, req);
// can't use direct equality, can't convert to object, but this works.
expect([...headers.entries()]).toEqual([...expHeaders.entries()]);
});
});
describe("setMirrorResponseHeaders", () => {
let setMirrorResponseHeaders = spec.__get__("setMirrorResponseHeaders");
let SPEC_MIRROR_HEADER_RESP_STATUS = spec.__get__(
"SPEC_MIRROR_HEADER_RESP_STATUS",
);
// Allow us to make a Response
Object.assign(global, makeServiceWorkerEnv());
test.each([
[
"sets response status header",
new Headers(),
new Response(null, { status: 200 }),
new Headers({ [SPEC_MIRROR_HEADER_RESP_STATUS]: "200" }),
],
[
"overrides response status header if present",
new Headers({ [SPEC_MIRROR_HEADER_RESP_STATUS]: "302" }),
new Response(null, { status: 200 }),
new Headers({ [SPEC_MIRROR_HEADER_RESP_STATUS]: "200" }),
],
[
"assigns other response headers from response",
new Headers(),
new Response(null, {
status: 200,
headers: { "content-length": "50", "x-foo": "foo" },
}),
new Headers({
[SPEC_MIRROR_HEADER_RESP_STATUS]: "200",
"x-spec-resp-head-content-length": "50",
"x-spec-resp-head-x-foo": "foo",
}),
],
[
"strips and overrides any headers from request with our prefix",
new Headers({
"x-spec-resp-head-content-length": "0",
"x-spec-resp-head-fake": "fake",
"x-spec-resp-head-x-foo": "foo",
"x-spec-resp-head": "root removed",
}),
new Response(null, {
status: 200,
headers: { "content-length": "50", "x-foo": "foo" },
}),
new Headers({
[SPEC_MIRROR_HEADER_RESP_STATUS]: "200",
"x-spec-resp-head-content-length": "50",
"x-spec-resp-head-x-foo": "foo",
}),
],
])(`makeSpecUrl: %s`, (_caseName, headers, resp, expHeaders) => {
setMirrorResponseHeaders(headers, resp);
expect([...headers.entries()]).toEqual([...expHeaders.entries()]);
});
});