initial commit (app returns google/startpage results)
This commit is contained in:
+676
@@ -0,0 +1,676 @@
|
||||
'use strict'
|
||||
|
||||
const {
|
||||
safeHTTPMethods,
|
||||
pathHasQueryOrFragment,
|
||||
hasSafeIterator,
|
||||
isValidHTTPToken
|
||||
} = require('../core/util')
|
||||
|
||||
const { serializePathWithQuery } = require('../core/util')
|
||||
|
||||
const MAX_DELTA_SECONDS = 2147483647
|
||||
const RESTRICTIVE_DIRECTIVE_NAMES = ['no-store', 'private', 'no-cache']
|
||||
const kInvalidCacheControlDirectives = Symbol('invalid cache-control directives')
|
||||
|
||||
function trimOWS (value) {
|
||||
return value.replace(/^[\t ]+|[\t ]+$/g, '')
|
||||
}
|
||||
|
||||
function arrayIncludes (array, value) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (array[i] === value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function trimOWSStart (value) {
|
||||
return value.replace(/^[\t ]+/, '')
|
||||
}
|
||||
|
||||
function trimOWSEnd (value) {
|
||||
return value.replace(/[\t ]+$/, '')
|
||||
}
|
||||
|
||||
function findUnescapedQuote (value, start) {
|
||||
let escaped = false
|
||||
for (let i = start; i < value.length; i++) {
|
||||
if (escaped) {
|
||||
escaped = false
|
||||
} else if (value[i] === '\\') {
|
||||
escaped = true
|
||||
} else if (value[i] === '"') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
function splitCacheControlHeaderValue (value) {
|
||||
const directives = []
|
||||
let start = 0
|
||||
let quoteStart = -1
|
||||
let inQuote = false
|
||||
let escaped = false
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (inQuote) {
|
||||
if (escaped) {
|
||||
escaped = false
|
||||
} else if (value[i] === '\\') {
|
||||
escaped = true
|
||||
} else if (value[i] === '"') {
|
||||
inQuote = false
|
||||
quoteStart = -1
|
||||
}
|
||||
} else if (value[i] === '"') {
|
||||
inQuote = true
|
||||
quoteStart = i
|
||||
} else if (value[i] === ',') {
|
||||
directives.push({ value: value.substring(start, i), fromMalformedQuote: false })
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!inQuote) {
|
||||
directives.push({ value: value.substring(start), fromMalformedQuote: false })
|
||||
return directives
|
||||
}
|
||||
|
||||
const tail = value.substring(start)
|
||||
const quoteOffset = quoteStart - start
|
||||
let tailStart = 0
|
||||
for (let i = 0; i < tail.length; i++) {
|
||||
if (tail[i] === ',') {
|
||||
directives.push({
|
||||
value: tail.substring(tailStart, i),
|
||||
fromMalformedQuote: tailStart > quoteOffset
|
||||
})
|
||||
tailStart = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
directives.push({
|
||||
value: tail.substring(tailStart),
|
||||
fromMalformedQuote: tailStart > quoteOffset
|
||||
})
|
||||
return directives
|
||||
}
|
||||
|
||||
function markInvalidCacheControlDirective (directives, key) {
|
||||
let invalidDirectives = directives[kInvalidCacheControlDirectives]
|
||||
|
||||
if (invalidDirectives === undefined) {
|
||||
invalidDirectives = new Set()
|
||||
Object.defineProperty(directives, kInvalidCacheControlDirectives, {
|
||||
value: invalidDirectives
|
||||
})
|
||||
}
|
||||
|
||||
invalidDirectives.add(key)
|
||||
}
|
||||
|
||||
function hasInvalidCacheControlDirective (directives, key) {
|
||||
return directives[kInvalidCacheControlDirectives]?.has(key) === true
|
||||
}
|
||||
|
||||
function getMalformedRestrictiveDirectiveName (key) {
|
||||
for (const directiveName of RESTRICTIVE_DIRECTIVE_NAMES) {
|
||||
if (
|
||||
key.startsWith(directiveName) &&
|
||||
key.length > directiveName.length &&
|
||||
!isValidHTTPToken(key[directiveName.length])
|
||||
) {
|
||||
return directiveName
|
||||
}
|
||||
}
|
||||
|
||||
let tokenOnlyKey = ''
|
||||
let hasInvalidTokenChar = false
|
||||
for (let i = 0; i < key.length; i++) {
|
||||
if (isValidHTTPToken(key[i])) {
|
||||
tokenOnlyKey += key[i]
|
||||
} else {
|
||||
hasInvalidTokenChar = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInvalidTokenChar && arrayIncludes(RESTRICTIVE_DIRECTIVE_NAMES, tokenOnlyKey)) {
|
||||
return tokenOnlyKey
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts
|
||||
*/
|
||||
function makeCacheKey (opts) {
|
||||
if (!opts.origin) {
|
||||
throw new Error('opts.origin is undefined')
|
||||
}
|
||||
|
||||
let fullPath = opts.path || '/'
|
||||
|
||||
if (opts.query && !pathHasQueryOrFragment(fullPath)) {
|
||||
fullPath = serializePathWithQuery(fullPath, opts.query)
|
||||
}
|
||||
|
||||
return {
|
||||
origin: opts.origin.toString(),
|
||||
method: opts.method,
|
||||
path: fullPath,
|
||||
headers: opts.headers
|
||||
}
|
||||
}
|
||||
|
||||
function appendHeader (headers, key, val) {
|
||||
const headerName = key.toLowerCase()
|
||||
const current = headers[headerName]
|
||||
const values = Array.isArray(val) ? val : [val]
|
||||
|
||||
if (current === undefined) {
|
||||
headers[headerName] = Array.isArray(val) ? val.slice() : val
|
||||
} else if (Array.isArray(current)) {
|
||||
current.push(...values)
|
||||
} else {
|
||||
headers[headerName] = [current, ...values]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string[] | string>}
|
||||
* @returns {Record<string, string[] | string>}
|
||||
*/
|
||||
function normalizeHeaders (opts) {
|
||||
let headers
|
||||
if (opts.headers == null) {
|
||||
headers = {}
|
||||
} else if (typeof opts.headers === 'object') {
|
||||
headers = {}
|
||||
|
||||
if (hasSafeIterator(opts.headers)) {
|
||||
for (const x of opts.headers) {
|
||||
if (!Array.isArray(x)) {
|
||||
throw new Error('opts.headers is not a valid header map')
|
||||
}
|
||||
const [key, val] = x
|
||||
if (typeof key !== 'string' || typeof val !== 'string') {
|
||||
throw new Error('opts.headers is not a valid header map')
|
||||
}
|
||||
appendHeader(headers, key, val)
|
||||
}
|
||||
} else {
|
||||
for (const key of Object.keys(opts.headers)) {
|
||||
appendHeader(headers, key, opts.headers[key])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('opts.headers is not an object')
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} key
|
||||
*/
|
||||
function assertCacheKey (key) {
|
||||
if (typeof key !== 'object') {
|
||||
throw new TypeError(`expected key to be object, got ${typeof key}`)
|
||||
}
|
||||
|
||||
for (const property of ['origin', 'method', 'path']) {
|
||||
if (typeof key[property] !== 'string') {
|
||||
throw new TypeError(`expected key.${property} to be string, got ${typeof key[property]}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (key.headers !== undefined && typeof key.headers !== 'object') {
|
||||
throw new TypeError(`expected headers to be object, got ${typeof key}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
function assertCacheValue (value) {
|
||||
if (typeof value !== 'object') {
|
||||
throw new TypeError(`expected value to be object, got ${typeof value}`)
|
||||
}
|
||||
|
||||
for (const property of ['statusCode', 'cachedAt', 'staleAt', 'deleteAt']) {
|
||||
if (typeof value[property] !== 'number') {
|
||||
throw new TypeError(`expected value.${property} to be number, got ${typeof value[property]}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value.statusMessage !== 'string') {
|
||||
throw new TypeError(`expected value.statusMessage to be string, got ${typeof value.statusMessage}`)
|
||||
}
|
||||
|
||||
if (value.headers != null && typeof value.headers !== 'object') {
|
||||
throw new TypeError(`expected value.rawHeaders to be object, got ${typeof value.headers}`)
|
||||
}
|
||||
|
||||
if (value.vary !== undefined && typeof value.vary !== 'object') {
|
||||
throw new TypeError(`expected value.vary to be object, got ${typeof value.vary}`)
|
||||
}
|
||||
|
||||
if (value.etag !== undefined && typeof value.etag !== 'string') {
|
||||
throw new TypeError(`expected value.etag to be string, got ${typeof value.etag}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control
|
||||
* @see https://www.iana.org/assignments/http-cache-directives/http-cache-directives.xhtml
|
||||
|
||||
* @param {string | string[]} header
|
||||
* @returns {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
|
||||
*/
|
||||
function parseCacheControlHeader (header) {
|
||||
/**
|
||||
* @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives}
|
||||
*/
|
||||
const output = {}
|
||||
const invalidNumericDirectives = new Set()
|
||||
const invalidNoArgumentDirectives = new Set()
|
||||
|
||||
const directives = splitCacheControlHeaderValue(Array.isArray(header) ? header.join(',') : header)
|
||||
|
||||
for (let i = 0; i < directives.length; i++) {
|
||||
const directiveRecord = directives[i]
|
||||
const directive = directiveRecord.value.toLowerCase()
|
||||
const fromMalformedQuote = directiveRecord.fromMalformedQuote
|
||||
const keyValueDelimiter = directive.indexOf('=')
|
||||
|
||||
let key
|
||||
let value
|
||||
let keyHasTrailingWhitespace = false
|
||||
let valueHasLeadingWhitespace = false
|
||||
if (keyValueDelimiter !== -1) {
|
||||
const rawKey = directive.substring(0, keyValueDelimiter)
|
||||
const rawValue = directive.substring(keyValueDelimiter + 1)
|
||||
|
||||
keyHasTrailingWhitespace = trimOWSEnd(rawKey) !== rawKey
|
||||
valueHasLeadingWhitespace = trimOWSStart(rawValue) !== rawValue
|
||||
key = trimOWS(rawKey)
|
||||
value = trimOWSStart(rawValue)
|
||||
} else {
|
||||
key = trimOWS(directive)
|
||||
}
|
||||
|
||||
const malformedRestrictiveDirectiveName = getMalformedRestrictiveDirectiveName(key)
|
||||
if (malformedRestrictiveDirectiveName !== undefined) {
|
||||
output[malformedRestrictiveDirectiveName] = true
|
||||
continue
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'min-fresh':
|
||||
case 'max-stale':
|
||||
case 'max-age':
|
||||
case 's-maxage':
|
||||
case 'stale-while-revalidate':
|
||||
case 'stale-if-error': {
|
||||
if (fromMalformedQuote || invalidNumericDirectives.has(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (value === undefined || keyHasTrailingWhitespace || valueHasLeadingWhitespace) {
|
||||
delete output[key]
|
||||
invalidNumericDirectives.add(key)
|
||||
markInvalidCacheControlDirective(output, key)
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
value.length >= 2 &&
|
||||
value[0] === '"' &&
|
||||
value[value.length - 1] === '"'
|
||||
) {
|
||||
value = value.substring(1, value.length - 1)
|
||||
}
|
||||
|
||||
if (!/^[0-9]+$/.test(value)) {
|
||||
delete output[key]
|
||||
invalidNumericDirectives.add(key)
|
||||
markInvalidCacheControlDirective(output, key)
|
||||
continue
|
||||
}
|
||||
|
||||
const parsedValue = Math.min(parseInt(value, 10), MAX_DELTA_SECONDS)
|
||||
|
||||
if (key === 'min-fresh') {
|
||||
if (!(key in output) || output[key] < parsedValue) {
|
||||
output[key] = parsedValue
|
||||
}
|
||||
} else if (!(key in output) || output[key] > parsedValue) {
|
||||
output[key] = parsedValue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'private':
|
||||
case 'no-cache': {
|
||||
if (fromMalformedQuote) {
|
||||
output[key] = true
|
||||
break
|
||||
}
|
||||
|
||||
if (value !== undefined && value.length === 0) {
|
||||
output[key] = true
|
||||
break
|
||||
}
|
||||
|
||||
if (value) {
|
||||
// The private and no-cache directives can be unqualified (aka just
|
||||
// `private` or `no-cache`) or qualified (w/ a value). When they're
|
||||
// qualified, it's a list of headers like `no-cache=header1`,
|
||||
// `no-cache="header1"`, or `no-cache="header1, header2"`
|
||||
// If we're given multiple headers, the comma messes us up since
|
||||
// we split the full header by commas. So, let's loop through the
|
||||
// remaining parts in front of us until we find one that contains a
|
||||
// closing quote. We can then skip the consumed quoted-list fragments and
|
||||
// continue parsing like normal.
|
||||
// https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2
|
||||
if (value[0] === '"') {
|
||||
// Something like `no-cache="some-header"` OR `no-cache="some-header, another-header"`.
|
||||
value = trimOWSEnd(value)
|
||||
|
||||
let fieldList = ''
|
||||
let lastQuotedPart = i
|
||||
let foundEndingQuote = false
|
||||
const closingQuote = findUnescapedQuote(value, 1)
|
||||
|
||||
if (closingQuote !== -1) {
|
||||
fieldList = value.substring(1, closingQuote)
|
||||
foundEndingQuote = true
|
||||
} else {
|
||||
// Something like `no-cache="some-header, another-header"`
|
||||
// This can still be something invalid, e.g. `no-cache="some-header, ...`
|
||||
const fieldListParts = [value.substring(1)]
|
||||
|
||||
for (let j = i + 1; j < directives.length; j++) {
|
||||
const nextPart = trimOWS(directives[j].value)
|
||||
const closingQuote = findUnescapedQuote(nextPart, 0)
|
||||
|
||||
lastQuotedPart = j
|
||||
|
||||
if (closingQuote !== -1) {
|
||||
fieldListParts.push(nextPart.substring(0, closingQuote))
|
||||
foundEndingQuote = true
|
||||
break
|
||||
}
|
||||
|
||||
fieldListParts.push(nextPart)
|
||||
}
|
||||
|
||||
fieldList = fieldListParts.join(',')
|
||||
}
|
||||
|
||||
if (!foundEndingQuote) {
|
||||
output[key] = true
|
||||
break
|
||||
}
|
||||
|
||||
i = lastQuotedPart
|
||||
|
||||
const headers = fieldList.split(',')
|
||||
let validFieldNames = true
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
headers[j] = trimOWS(headers[j])
|
||||
if (!isValidHTTPToken(headers[j])) {
|
||||
validFieldNames = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!validFieldNames) {
|
||||
output[key] = true
|
||||
} else if (output[key] !== true) {
|
||||
if (key in output) {
|
||||
output[key] = output[key].concat(headers)
|
||||
} else {
|
||||
output[key] = headers
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Something like `no-cache=some-header`
|
||||
const fieldName = trimOWS(value)
|
||||
|
||||
if (!isValidHTTPToken(fieldName)) {
|
||||
output[key] = true
|
||||
} else if (output[key] !== true) {
|
||||
if (key in output) {
|
||||
output[key] = output[key].concat(fieldName)
|
||||
} else {
|
||||
output[key] = [fieldName]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case 'public':
|
||||
case 'must-revalidate':
|
||||
case 'proxy-revalidate':
|
||||
case 'immutable':
|
||||
case 'no-transform':
|
||||
case 'must-understand':
|
||||
case 'only-if-cached':
|
||||
if (fromMalformedQuote || invalidNoArgumentDirectives.has(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
// These are qualified (something like `public=...`) when they aren't
|
||||
// allowed to be, skip all instances of the malformed directive.
|
||||
delete output[key]
|
||||
invalidNoArgumentDirectives.add(key)
|
||||
continue
|
||||
}
|
||||
|
||||
output[key] = true
|
||||
break
|
||||
case 'no-store':
|
||||
output[key] = true
|
||||
break
|
||||
default:
|
||||
// Ignore unknown directives as per https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.3-1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} varyHeader Vary header from the server
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function splitVaryHeader (varyHeader) {
|
||||
const values = Array.isArray(varyHeader) ? varyHeader : [varyHeader]
|
||||
const output = []
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const parts = values[i].split(',')
|
||||
for (let j = 0; j < parts.length; j++) {
|
||||
output.push(parts[j])
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} varyHeader Vary header from the server
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasVaryStar (varyHeader) {
|
||||
const values = splitVaryHeader(varyHeader)
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (trimOWS(values[i]).indexOf('*') !== -1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} varyHeader Vary header from the server
|
||||
* @param {Record<string, string | string[]>} headers Request headers
|
||||
* @returns {Record<string, string | string[] | null> | undefined}
|
||||
*/
|
||||
function parseVaryHeader (varyHeader, headers) {
|
||||
if (hasVaryStar(varyHeader)) {
|
||||
return headers
|
||||
}
|
||||
|
||||
const output = /** @type {Record<string, string | string[] | null>} */ ({})
|
||||
|
||||
const varyingHeaders = splitVaryHeader(varyHeader)
|
||||
|
||||
for (const header of varyingHeaders) {
|
||||
const trimmedHeader = trimOWS(header).toLowerCase()
|
||||
|
||||
if (trimmedHeader.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!isValidHTTPToken(trimmedHeader)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const headerValue = headers[trimmedHeader]
|
||||
output[trimmedHeader] = Array.isArray(headerValue) ? headerValue.slice() : headerValue ?? null
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} varyHeader Vary header from the server
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidOrWildcardVaryHeader (varyHeader) {
|
||||
return hasVaryStar(varyHeader) || parseVaryHeader(varyHeader, {}) === undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this deviates from the spec a little. Empty etags ("", W/"") are valid,
|
||||
* however, including them in cached resposnes serves little to no purpose.
|
||||
*
|
||||
* @see https://www.rfc-editor.org/rfc/rfc9110.html#name-etag
|
||||
*
|
||||
* @param {string} etag
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEtagUsable (etag) {
|
||||
if (etag.length <= 2) {
|
||||
// Shortest an etag can be is two chars (just ""). This is where we deviate
|
||||
// from the spec requiring a min of 3 chars however
|
||||
return false
|
||||
}
|
||||
|
||||
if (etag[0] === '"' && etag[etag.length - 1] === '"') {
|
||||
// ETag: ""asd123"" or ETag: "W/"asd123"", kinda undefined behavior in the
|
||||
// spec. Some servers will accept these while others don't.
|
||||
// ETag: "asd123"
|
||||
return !(etag[1] === '"' || etag.startsWith('"W/'))
|
||||
}
|
||||
|
||||
if (etag.startsWith('W/"') && etag[etag.length - 1] === '"') {
|
||||
// ETag: W/"", also where we deviate from the spec & require a min of 3
|
||||
// chars
|
||||
// ETag: for W/"", W/"asd123"
|
||||
return etag.length !== 4
|
||||
}
|
||||
|
||||
// Anything else
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} store
|
||||
* @returns {asserts store is import('../../types/cache-interceptor.d.ts').default.CacheStore}
|
||||
*/
|
||||
function assertCacheStore (store, name = 'CacheStore') {
|
||||
if (typeof store !== 'object' || store === null) {
|
||||
throw new TypeError(`expected type of ${name} to be a CacheStore, got ${store === null ? 'null' : typeof store}`)
|
||||
}
|
||||
|
||||
for (const fn of ['get', 'createWriteStream', 'delete']) {
|
||||
if (typeof store[fn] !== 'function') {
|
||||
throw new TypeError(`${name} needs to have a \`${fn}()\` function`)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {unknown} methods
|
||||
* @returns {asserts methods is import('../../types/cache-interceptor.d.ts').default.CacheMethods[]}
|
||||
*/
|
||||
function assertCacheMethods (methods, name = 'CacheMethods') {
|
||||
if (!Array.isArray(methods)) {
|
||||
throw new TypeError(`expected type of ${name} needs to be an array, got ${methods === null ? 'null' : typeof methods}`)
|
||||
}
|
||||
|
||||
if (methods.length === 0) {
|
||||
throw new TypeError(`${name} needs to have at least one method`)
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
if (!arrayIncludes(safeHTTPMethods, method)) {
|
||||
throw new TypeError(`element of ${name}-array needs to be one of following values: ${safeHTTPMethods.join(', ')}, got ${method}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string key for request deduplication purposes.
|
||||
* This key is used to identify in-flight requests that can be shared.
|
||||
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
|
||||
* @param {Set<string>} [excludeHeaders] Set of lowercase header names to exclude from the key
|
||||
* @returns {string}
|
||||
*/
|
||||
function makeDeduplicationKey (cacheKey, excludeHeaders) {
|
||||
// Use JSON.stringify to produce a collision-resistant key.
|
||||
// Previous format used `:` and `=` delimiters without escaping, which
|
||||
// allowed different header sets to produce identical keys (e.g.
|
||||
// {a:"x:b=y"} vs {a:"x", b:"y"}). See: https://github.com/nodejs/undici/issues/5012
|
||||
const headers = {}
|
||||
|
||||
if (cacheKey.headers) {
|
||||
const sortedHeaders = Object.keys(cacheKey.headers).sort()
|
||||
for (const header of sortedHeaders) {
|
||||
// Skip excluded headers
|
||||
if (excludeHeaders?.has(header.toLowerCase())) {
|
||||
continue
|
||||
}
|
||||
headers[header] = cacheKey.headers[header]
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify([cacheKey.origin, cacheKey.method, cacheKey.path, headers])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makeCacheKey,
|
||||
normalizeHeaders,
|
||||
assertCacheKey,
|
||||
assertCacheValue,
|
||||
parseCacheControlHeader,
|
||||
hasInvalidCacheControlDirective,
|
||||
parseVaryHeader,
|
||||
hasVaryStar,
|
||||
isInvalidOrWildcardVaryHeader,
|
||||
isEtagUsable,
|
||||
assertCacheMethods,
|
||||
assertCacheStore,
|
||||
makeDeduplicationKey
|
||||
}
|
||||
Reference in New Issue
Block a user