UNPKG

@specprotected/spec-proxy-fastly-worker

Version:

Spec Proxy implementation for Fastly Compute@Edge Workers

215 lines (192 loc) 6.86 kB
// 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; }