UNPKG

@fastly/esi

Version:

ESI implementation for JavaScript, using the modern fetch and streaming APIs.

213 lines (212 loc) 7.17 kB
/* * Copyright Fastly, Inc. * Licensed under the MIT license. See LICENSE file for details. */ import { quoteString, unquoteString } from "./util.js"; const NUMBER_TEST = /^(\d+(\.\d*)?|\.\d+)$/; export function parseAsNumber(val) { if (val === undefined || !NUMBER_TEST.test(val)) { return undefined; } return parseInt('0' + val, 10); } export class EsiStringVariable { value; constructor(value) { this.value = value; } getSubValue(_key) { return undefined; } getValue() { return quoteString(this.value); } } export class EsiListVariable { value; fn; map; constructor(value, fn) { this.value = value; this.fn = fn; } getSubValue(key) { if (this.map === undefined) { this.map = this.fn(this.value); } return (this.map[key] ?? false) ? 'true' : 'false'; } getValue() { return quoteString(this.value); } } export class EsiDictionaryVariable { value; fn; map; constructor(value, fn) { this.value = value; this.fn = fn; } getSubValue(key) { if (this.map === undefined) { this.map = this.fn(this.value); } const value = this.map[key] ?? ''; return quoteString(value); } getValue() { return quoteString(this.value); } } export class EsiAcceptLanguageVariable extends EsiListVariable { constructor(value) { super(value, (value) => { const langs = value.split(',') .map(seg => seg.split(';')[0].trim()) .filter(Boolean); const map = {}; for (const lang of langs) { map[decodeURIComponent(lang)] = true; } return map; }); } } export class EsiCookieVariable extends EsiDictionaryVariable { constructor(value) { super(value, (value) => { const cookieEntries = value.split(';') .map(seg => { const pieces = seg.split('='); const key = pieces.shift(); return [key, pieces.join('=')]; }); const map = {}; for (const [key, value] of cookieEntries) { if (key == null || value == null) { continue; } const k = key.trim(); const v = value.trim(); if (k === '' || v === '') { continue; } map[decodeURIComponent(k)] = decodeURIComponent(v); } return map; }); } } export class EsiQueryStringVariable extends EsiDictionaryVariable { constructor(value) { super(value, (value) => { const map = {}; for (const [k, v] of new URLSearchParams(value)) { map[k] = v; } return map; }); } } const USER_AGENT_REGEX = /^(?<browser>[^\/]+)\/(?<version>\d+(\.\d*))/; export class EsiUserAgentVariable extends EsiDictionaryVariable { constructor(value) { super(value, (value) => { let browser = 'OTHER'; let version = undefined; let os = 'OTHER'; if (value.includes('Windows')) { os = 'WIN'; } else if (value.includes('Mac OS X') || value.includes('Mac_PowerPC')) { os = 'MAC'; } else if (value.includes('Linux') || value.includes('Unix') || value.includes('BSD') || value.includes('CrOS')) { os = 'UNIX'; } const matchResult = USER_AGENT_REGEX.exec(value); if (matchResult != null) { if (matchResult.groups?.['browser']?.toUpperCase() === 'MOZILLA') { browser = 'MOZILLA'; } version = matchResult.groups?.['version']; } if (value.includes('MSIE') || value.includes('Trident/')) { browser = 'MSIE'; } return { browser, version, os }; }); } } export class EsiVariables { values = {}; constructor(url, headers) { const httpAcceptLanguageValue = headers?.get('accept-language') ?? ''; this.values['HTTP_ACCEPT_LANGUAGE'] = new EsiAcceptLanguageVariable(httpAcceptLanguageValue); const httpCookieValue = headers?.get('cookie') ?? ''; this.values['HTTP_COOKIE'] = new EsiCookieVariable(httpCookieValue); const httpHostValue = headers?.get('host'); if (httpHostValue != null) { this.values['HTTP_HOST'] = new EsiStringVariable(httpHostValue); } const httpRefererValue = headers?.get('referer'); if (httpRefererValue != null) { this.values['HTTP_REFERER'] = new EsiStringVariable(httpRefererValue); } const httpUserAgentValue = headers?.get('user-agent'); if (httpUserAgentValue != null) { this.values['HTTP_USER_AGENT'] = new EsiUserAgentVariable(httpUserAgentValue); } let queryStringValue = url?.search; while (queryStringValue?.startsWith('?')) { queryStringValue = queryStringValue.slice(1); } if (queryStringValue != null) { this.values['QUERY_STRING'] = new EsiQueryStringVariable(queryStringValue); } } getValue(name, subKey = null) { if (subKey == null) { return this.values[name]?.getValue(); } return this.values[name]?.getSubValue(subKey); } } function evaluateEsiVariableValue(groups, vars) { const varName = groups['varName']; const subkeyName = groups['subkeyName'] ?? null; const value = vars?.getValue(varName, subkeyName); if (value === undefined || value === '' || value === 'false') { const defaultValue = groups['defaultValue1'] ?? groups['defaultValue2']; if (defaultValue != null) { return quoteString(defaultValue); } } return value; } const ESI_VARIABLES_REGEX = /\$\((?<varName>[-_A-Z]+)(?:\{(?<subkeyName>[-_A-Za-z0-9]+)})?(?:\|(?:(?<defaultValue1>[^\s']+)|'(?<defaultValue2>[^']*)'))?\)/g; export function applyEsiVariables(input, vars) { if (input == null) { return undefined; } return input.replace(ESI_VARIABLES_REGEX, (_, ...args) => { const groups = args[args.length - 1]; const value = evaluateEsiVariableValue(groups, vars); if (value === undefined || value === '' || value === 'true' || value === 'false') { return ''; } return unquoteString(value); }); } const ESI_VARIABLE_REGEX = /^\$\((?<varName>[-_A-Z]+)(?:\{(?<subkeyName>[-_A-Za-z0-9]+)})?(?:\|(?:(?<defaultValue1>[^\s']+)|'(?<defaultValue2>[^']*)'))?\)$/; export function evaluateEsiVariable(input, vars) { const match = ESI_VARIABLE_REGEX.exec(input ?? ''); if (match == null || match.groups == null) { throw new Error('invalid variable format'); } return evaluateEsiVariableValue(match?.groups, vars); }