All files / proxy/utils headers.ts

96.15% Statements 75/78
92.59% Branches 25/27
100% Functions 11/11
95.89% Lines 70/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248    10x 10x   10x 10x   10x                                                           10x   10x                     10x   10x                     10x         68x 27x   41x   41x 41x 41x 40x   41x   41x     43x                   10x 70x 204x 204x 87x 28x         176x 74x 74x 42x 42x     74x   176x                     10x       54x   54x     115x 20x     95x 10x           85x 85x                 54x                       176x 90x   86x 168x 12x     74x                       10x 115x 12x   103x 204x 8x     95x                 10x 491x 475x     16x                   10x 491x 491x   491x 268x   223x                 10x 66x   66x             66x    
import { CloudFrontHeaders, CloudFrontRequest } from 'aws-lambda'
import { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http'
import { filterCookie } from './cookie'
import { updateCacheControlHeader } from './cache-control'
import { CustomerVariables } from './customer-variables/customer-variables'
import { getPreSharedSecret } from './customer-variables/selectors'
import { TTLCache } from './cache'
 
export const BLACKLISTED_HEADERS = new Set([
  'age',
  'connection',
  'expect',
  'keep-alive',
  'proxy-authenticate',
  'proxy-authorization',
  'proxy-connection',
  'trailer',
  'upgrade',
  'x-accel-buffering',
  'x-accel-charset',
  'x-accel-limit-rate',
  'x-accel-redirect',
  'x-amzn-auth',
  'x-amzn-cf-billing',
  'x-amzn-cf-id',
  'x-amzn-cf-xff',
  'x-amzn-errortype',
  'x-amzn-fle-profile',
  'x-amzn-header-count',
  'x-amzn-header-order',
  'x-amzn-lambda-integration-tag',
  'x-amzn-requestid',
  'x-cache',
  'x-forwarded-proto',
  'x-real-ip',
  'strict-transport-security',
])
 
export const BLACKLISTED_HEADERS_PREFIXES = ['x-edge-', 'x-amz-cf-']
 
const READ_ONLY_RESPONSE_HEADERS = new Set([
  'accept-encoding',
  'content-length',
  'if-modified-since',
  'if-none-match',
  'if-range',
  'if-unmodified-since',
  'transfer-encoding',
  'via',
])
 
const READ_ONLY_REQUEST_HEADERS = new Set(['content-length', 'host', 'transfer-encoding', 'via'])
 
export const CACHE_CONTROL_HEADER_NAME = 'cache-control'
 
/**
 * Prepares the headers for an ingress request by filtering incoming request headers
 * and appending necessary custom headers based on the provided parameters.
 *
 * @param {CloudFrontRequest} request - The incoming request object from CloudFront, containing headers and client-related information.
 * @param {CustomerVariables} variables - The customer-specific variables required for fetching the pre-shared secret.
 * @param {boolean} isAuthorisedMethodCall - A flag indicating whether the request method is for authorised request (only POST for now).
 * @return {Promise<OutgoingHttpHeaders>} A promise that resolves to an object containing the modified headers for the request.
 */
export async function prepareHeadersForIngressRequest(
  request: CloudFrontRequest,
  variables: CustomerVariables,
  isAuthorisedMethodCall: boolean
): Promise<OutgoingHttpHeaders> {
  if (!isAuthorisedMethodCall) {
    return filterRequestHeaders(request, true)
  }
  const headers = filterRequestHeaders(request)
 
  headers['fpjs-proxy-client-ip'] = request.clientIp
  const preSharedSecret = await getPreSharedSecret(variables)
  if (preSharedSecret) {
    headers['fpjs-proxy-secret'] = preSharedSecret
  }
  headers['fpjs-proxy-forwarded-host'] = getHost(request)
 
  return headers
}
 
export const getHost = (request: CloudFrontRequest) => request.headers['host'][0].value
 
/**
 * Filters request headers to allow only permitted headers for outgoing requests.
 * Optionally drops all cookies or filters specific cookies based on a condition.
 *
 * @param {CloudFrontRequest} request - The original CloudFront request containing headers.
 * @param {boolean} [dropCookies=false] - A flag to determine whether to remove all cookies from the headers.
 * @return {OutgoingHttpHeaders} An object containing the filtered headers to be sent with the outgoing request.
 */
export function filterRequestHeaders(request: CloudFrontRequest, dropCookies: boolean = false): OutgoingHttpHeaders {
  return Object.entries(request.headers).reduce((result: { [key: string]: string }, [name, value]) => {
    const headerName = name.toLowerCase()
    if (dropCookies) {
      if (headerName === 'cookie') {
        return result
      }
    }
 
    // Lambda@Edge function can't add read-only headers from a client request to Ingress API request
    if (isHeaderAllowedForRequest(headerName)) {
      let headerValue = value[0].value
      if (headerName === 'cookie') {
        headerValue = headerValue.split(/; */).join('; ')
        headerValue = filterCookie(headerValue, (key) => key === '_iidt')
      }
 
      result[headerName] = headerValue
    }
    return result
  }, {})
}
 
/**
 * Updates the response headers based on the provided headers object and an optional flag to override the Cache-Control header.
 *
 * @param {IncomingHttpHeaders} headers - The incoming HTTP headers from the request. These are processed to generate the response headers.
 * @param {boolean} [overrideCacheControl=false] - A flag indicating whether to override the Cache-Control header if it exists. Defaults to `false`.
 * @return {CloudFrontHeaders} The updated headers formatted as CloudFront-compatible response headers.
 */
export function updateResponseHeaders(
  headers: IncomingHttpHeaders,
  overrideCacheControl: boolean = false
): CloudFrontHeaders {
  const resultHeaders: CloudFrontHeaders = {}
 
  for (const [key, value] of Object.entries(headers)) {
    // Lambda@Edge function can't add read-only headers to response to CloudFront
    // So, such headers from IngressAPI response are filtered out before return the response to CloudFront
    if (!isHeaderAllowedForResponse(key)) {
      continue
    }
 
    if (overrideCacheControl && key == CACHE_CONTROL_HEADER_NAME && typeof value === 'string') {
      resultHeaders[CACHE_CONTROL_HEADER_NAME] = [
        {
          key: CACHE_CONTROL_HEADER_NAME,
          value: updateCacheControlHeader(value),
        },
      ]
    } else if (value) {
      resultHeaders[key] = [
        {
          key: key,
          value: value.toString(),
        },
      ]
    }
  }
 
  return resultHeaders
}
 
/**
 * Determines whether a given header is allowed to be used in a request.
 * Checks against a list of read-only headers, blacklisted headers,
 * as well as headers with specific blacklisted prefixes.
 *
 * @param {string} headerName - The name of the header to check.
 * @return {boolean} Returns true if the header is allowed, otherwise false.
 */
function isHeaderAllowedForRequest(headerName: string): boolean {
  if (READ_ONLY_REQUEST_HEADERS.has(headerName) || BLACKLISTED_HEADERS.has(headerName)) {
    return false
  }
  for (let i = 0; i < BLACKLISTED_HEADERS_PREFIXES.length; i++) {
    if (headerName.startsWith(BLACKLISTED_HEADERS_PREFIXES[i])) {
      return false
    }
  }
  return true
}
 
/**
 * Determines whether a given header name is allowed to be included in the response.
 *
 * The method checks against a set of read-only or blacklisted headers, as well as headers
 * that start with specific blacklisted prefixes, to decide if the header is permitted.
 *
 * @param {string} headerName - The name of the header to be checked.
 * @return {boolean} Returns true if the header is allowed, otherwise false.
 */
export function isHeaderAllowedForResponse(headerName: string): boolean {
  if (READ_ONLY_RESPONSE_HEADERS.has(headerName) || BLACKLISTED_HEADERS.has(headerName)) {
    return false
  }
  for (let i = 0; i < BLACKLISTED_HEADERS_PREFIXES.length; i++) {
    if (headerName.startsWith(BLACKLISTED_HEADERS_PREFIXES[i])) {
      return false
    }
  }
  return true
}
 
/**
 * Extracts the origin information from the CloudFront request headers.
 *
 * @param {Object} params - The parameters object.
 * @param {CloudFrontRequest} params.origin - The origin information from the CloudFront request.
 */
export function getOriginForHeaders({ origin }: CloudFrontRequest) {
  if (origin?.s3) {
    return origin.s3
  }
 
  return origin?.custom
}
 
/**
 * Retrieves the value of a specified header from the custom headers of a CloudFront request.
 *
 * @param {CloudFrontRequest} request - The CloudFront request object containing the headers.
 * @param {string} name - The name of the header to retrieve the value for.
 * @return {string|null} The value of the specified header if it exists, or null if the header is not found.
 */
export function getHeaderValue(request: CloudFrontRequest, name: string): string | null {
  const origin = getOriginForHeaders(request)
  const headers = origin?.customHeaders
 
  if (!headers?.[name]) {
    return null
  }
  return headers[name][0].value
}
 
/**
 * Retrieves the secret cache time-to-live (TTL) value in milliseconds from the request headers.
 *
 * @param {CloudFrontRequest} request - The CloudFront request object containing headers.
 * @return {number|undefined} The parsed TTL value in milliseconds if present and valid; otherwise, undefined.
 */
export function getSecretCacheTtlMs(request: CloudFrontRequest): number | undefined {
  const value = getHeaderValue(request, 'fpjs_proxy_secret_cache_ttl_ms')
 
  Iif (value) {
    const parsedValue = parseInt(value, 10)
    Iif (TTLCache.isValidTTL(parsedValue)) {
      return parsedValue
    }
  }
 
  return undefined
}