@specprotected/spec-proxy-fastly-worker
Version:
Spec Proxy implementation for Fastly Compute@Edge Workers
215 lines (192 loc) • 6.86 kB
text/typescript
// This is a wrapper module to support Fastly Compute@Edge workers. We isolate
// this code to help shield implementation details from the inner workings of
// the library and help keep a standard interface while we change how our
// API operates.
/// <reference types="@fastly/js-compute" />
import * as spec from "@specprotected/spec-proxy-service-worker";
import { Backend } from "fastly:backend";
export interface HostReplacement {
/**
* The pattern to find within the hostname
*/
find: string;
/**
* The text to replace the found pattern within the hostname.
* This can contain capture group references when regex = true.
*/
replace: string;
/**
* Set to true to enable a regular expression search. Since regex
* searches are slightly slower, this is not enabled by default.
*/
regex: boolean;
}
export interface SpecConfiguration extends spec.SpecConfiguration {
/**
* Set to true to enable dynamicBackends support, which requires a Fastly
* enabled feature on your account. Contact sales@fastly.com to enable it.
* Dynamic Backends do not require you to configure a backend for each service
* you would like to contact, nor does it require that you call setFastlyBackends
* prior to execution of the worker request handler.
*/
dynamicBackends?: boolean;
/**
* Specify a header that contains the originating Host. This is useful if the
* host header is obscured by another Fastly Service.
*/
alternateHostHeader?: string;
/**
* Specify `HostReplacement` objects which define a find/replace pattern to
* replace the value of an incoming hostname. This can be used to algorithmically
* map external hostnames to origin hostnames, as in converting `auth.customer.com`
* into `auth.origin-customer.com`
*/
hostReplacements?: [HostReplacement];
}
let backendMap: Record<string, string>;
export function setFastlyBackends(map: Record<string, string>) {
backendMap = map;
}
export function addFastlyBackend(host: string, backend: string) {
backendMap[host] = backend;
}
class FastlyRequest extends Request {
constructor(
input: RequestInfo,
init?: RequestInit,
original?: Request,
config?: SpecConfiguration,
) {
let url: URL;
// Note: type RequestInfo = string | Request;
if (typeof input === "string") {
url = new URL(input as string);
}
// It's a Request
else {
url = new URL((input as Request).url);
}
// Assignment is dependent on the dynamicBackends setting
let backend: Backend | string;
if (config?.dynamicBackends) {
let host: string | null = null;
let headers = new Headers(init?.headers || original?.headers);
// If we have an alternate host header specification, we can use that as the
// intended destination origin host
if (config?.alternateHostHeader) {
// Build a Headers object, which handles the various type interpretations
// of the HeadersInit type, or the originating request's headers
host = headers.get(config.alternateHostHeader);
}
// Otherwise use the Host header, if the host header isn't present
// Fastly doesn't work, this should always be present.
else {
host = headers.get("host");
}
host = replaceHost(host, config?.hostReplacements);
// Special case our Proxy url, which needs the originating hostname as a prefix
// where in the Dynamic Backends configuration the hostname prefix will be the
// Spec Proxy Fastly Service host.
//
// Note: When we create the request to .spec-internal.com in the common library,
// we DO NOT change the Host header, so it would remain www.customer.com,
// though the url's hostname is www.customer.com.spec-internal.com.
//
// From the Fastly Service, this url would be something akin to
// `spec-proxy.customer.com.spec-internal.com` and we need to this become
// `destination.customer.com.spec-internal.com`
if (host && url.hostname.endsWith(".spec-internal.com")) {
// Suffix the destination host with .spec-internal.com
// Allowing Spec Proxy to understand the originating host.
host += ".spec-internal.com";
} else {
host = url.hostname;
}
backend = new Backend({
name: `${host}_dynamic_backend`,
target: host,
hostOverride: host,
certificateHostname: host,
sniHostname: host,
useSSL: true,
tlsMinVersion: 1.3,
tlsMaxVersion: 1.3,
});
} else {
backend = backendMap[url.hostname];
}
// There's an error here because Fastly doesn't provide types
// for Dynamic Backends
// @ts-expect-error
init = {
...init,
...{
backend,
// no cacheOverride accessor on Requests for fastly types...
cacheOverride: (original as any)?.cacheOverride,
},
};
super(input, init);
}
}
export function specProxyProcessRequest(
event: FetchEvent,
config: spec.SpecConfiguration = {},
): Request {
/* Fastly _REQUIRES_ a backend in order to route the request, so
* we always need to return a new request, even when we're disabled
* so that the request contains the backend
*/
if (config?.disableSpecProxy) {
let request = event.request;
return new FastlyRequest(
request.url,
{
body: request.body,
headers: request.headers,
method: request.method,
redirect: request.redirect,
},
request,
config,
);
}
return spec.specProxyProcessRequest(event, config, FastlyRequest);
}
export function specProxyProcessResponse(
request: Request,
response: Response,
): Response {
return spec.specProxyProcessResponse(request, response);
}
export async function specProxyProcess(
event: FetchEvent,
config: spec.SpecConfiguration,
) {
return spec.specProxyProcess(event, config, FastlyRequest);
}
function replaceHost(
host: string | null,
replacements?: [HostReplacement],
): string | null {
// validate parameters
if (!host || !replacements) {
return host;
}
for (let replacement of replacements) {
// If the specified pattern is regex, use `search` to determine if there's
// a match. If we were to use `search` on a non-regex pattern, the `.` in
// the replacement would match "any character" which could be problematic
if (replacement.regex) {
let pattern = new RegExp(replacement.find);
// if the pattern is found, return its replacement
if (host.search(pattern) >= 0) {
return host.replace(pattern, replacement.replace);
}
} else if (host.indexOf(replacement.find) >= 0) {
return host.replace(replacement.find, replacement.replace);
}
}
// no replacement was made
return host;
}