@specprotected/spec-proxy-service-worker
Version:
Server Worker API implementation for integrating with Spec Proxy from an Edge Worker
410 lines (363 loc) • 14.1 kB
text/typescript
import { jest } from "@jest/globals";
import rewire from "rewire";
import makeServiceWorkerEnv from "service-worker-mock";
import { parse as parseCookies } from "cookie";
import { specProxyProcessRequest, specProxyProcessResponse } from "./index";
// 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());
// 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 }),
).toBe(global.simple_event.request);
expect(
specProxyProcessRequest(global.simple_event, { disableSpecProxy: false }),
).not.toBe(global.simple_event.request);
});
test("traffic split", () => {
expect(
specProxyProcessRequest(global.simple_event, { percentageOfIPs: 0 }),
).toBe(global.simple_event.request);
expect(
specProxyProcessRequest(global.simple_event, { percentageOfIPs: 100 }),
).not.toBe(global.simple_event.request);
});
test("proxy forks request in listening mode", () => {
expect(specProxyProcessRequest(global.simple_event)).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 listening mode, Host header of mirrored request is unchanged", () => {
expect(specProxyProcessRequest(global.simple_event)).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,
});
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,
});
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,
});
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,
});
expect(request).toEqual(global.simple_event.request);
expect(global.simple_event.waitUntil.mock.calls.length).toEqual(1);
});
test("response sets SpecID cookie", () => {
let response = new Response(null, {
status: 200,
headers: {
"X-Forwarded-For": "127.0.0.1",
Host: "spectrust.com",
},
});
response = specProxyProcessResponse(
global.simple_event.request,
response,
{},
);
let setCookie = parseCookies(response.headers.get("Set-Cookie"));
expect(setCookie).toHaveProperty("x-spec-id");
expect(setCookie["Max-Age"]).toEqual("320000000");
// now assert that we don't assign a new cookie if one exists!
response = new Response(null, {
status: 200,
headers: {
"X-Forwarded-For": "127.0.0.1",
Host: "spectrust.com",
},
});
global.simple_event.request.headers.set("Cookie", "x-spec-id=abcd");
response = specProxyProcessResponse(global.simple_event.request, response);
let newSetCookie = response.headers.get("Set-Cookie");
expect(newSetCookie).toBeNull();
});
test("response set SpecID doesn't erase other set-cookies", () => {
let response = new Response(null, {
status: 200,
headers: {
"X-Forwarded-For": "127.0.0.1",
Host: "spectrust.com",
"Set-Cookie": "a=cookie",
},
});
response = specProxyProcessResponse(
global.simple_event.request,
response,
{},
);
expect(response.headers.get("Set-Cookie").split(",")).toHaveLength(2);
});
test("response doesn't set SpecID in inline mode", () => {
let response = new Response(null, {
status: 200,
headers: {
"X-Forwarded-For": "127.0.0.1",
Host: "spectrust.com",
},
});
response = specProxyProcessResponse(global.simple_event.request, response, {
inlineMode: true,
});
expect(response.headers.get("Set-Cookie")).toBeNull();
});
});
//---------------------------------------------
// Private Function Testing
//---------------------------------------------
// "rewire" the library module to allow us to reach private functions
let spec = rewire("./dist/index.js");
// setup the jest global mock
spec.__set__({ self: global.self });
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);
});
});