All files / worker/utils request.ts

100% Statements 28/28
100% Branches 24/24
100% Functions 3/3
100% Lines 28/28

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                                                                                    73x                         73x 73x 7x     66x   1x       65x 65x   2x 2x       63x 33x       30x 1x     29x                           41x 41x 26x           15x       8x 8x 5x         3x       12x   12x 10x 4x     2x      
import { AGENT_DATA_HEADER, SIGNALS_KEY } from '../../shared/const'
import { appendHeaderValue } from './headers'
 
interface CopyRequestParams {
  // The original request to be copied
  request: Request
  // Object containing custom settings to apply to the new Request
  init: RequestInit
  // Optional parameter to specify a new URL for the copied Request. If empty, the original request URL is used.
  url?: string | URL
}
 
/**
 * Creates a new Request object by copying an existing one and optionally modifying its URL.
 * Using `Request.clone()` in Cloudflare runtime is not recommended: https://developers.cloudflare.com/workers/examples/modify-request-property/
 *
 * @return {Request} A new Request object based on the original, with any modifications applied.
 *
 * @example Modifing headers in a request.
 * ```typescript
 * const request = new Request('https://example.com/', { headers: { 'Original-Header': 'value' }, method: 'GET' })
 *
 * // In case if you'd like to completly overwrite headers, use 'new Headers()' instead.
 * const updatedHeaders = new Headers(request.headers)
 * updatedHeaders.set('X-New-Header', 'new-value')
 *
 * const modifiedRequest = copyRequest({
 *   request,
 *   init: {
 *     // Updates request headers with new values
 *     headers: updatedHeaders,
 *     // Modify request method
 *     method: 'POST',
 *   },
 * })
 *
 * console.log(modifiedRequest.headers.get('X-New-Header')) // 'new-value'
 * console.log(modifiedRequest.headers.get('Original-Header')) // 'value'
 * ```
 */
export function copyRequest({ request, init, url }: CopyRequestParams): Request<unknown, IncomingRequestCfProperties> {
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
  return new Request(url ?? request.url, new Request(request, init)) as unknown as Request<
    unknown,
    IncomingRequestCfProperties
  >
}
 
/**
 * @param request the `Request` to examine
 *
 * @return if the request is a cross-origin request, the normalized origin value from the `Origin` header, as a URL
 *         if the request is a same-origin request or the origin value is invalid, null is returned
 */
export function getCrossOriginUrl(request: Request): URL | null {
  const originHeader = request.headers.get('Origin')
  if (!originHeader) {
    return null
  }
 
  if (originHeader === 'null') {
    // Special handling for the special "null" origin
    return null
  }
 
  let originUrl: URL
  try {
    originUrl = new URL(originHeader)
  } catch (e) {
    console.debug('Origin value is not a valid URL')
    return null
  }
 
  // Validate that the originHeader is only an origin (protocol://host[:port])
  if (originHeader.endsWith('/') || originUrl.toString() !== `${originUrl.origin}/`) {
    return null
  }
 
  // If the Origin header matches the request's origin, it's same-origin
  if (originUrl.origin === new URL(request.url).origin) {
    return null
  }
 
  return originUrl
}
 
/**
 * Sets the CORS headers needed to allow the browser to expose the response to an instrumented, cross-origin
 * request to the instrumentation script and JS app.
 *
 * The `Access-Control-Allow-Origin` header field from the response is used to determine if updates are
 * required.
 *
 * @param request the `Request` being handled
 * @param originResponseHeaders the headers from the origin `Response`, to be updated by this function
 */
export function setCorsHeadersForInstrumentation(request: Request, originResponseHeaders: Headers) {
  const allowedOrigin = originResponseHeaders.get('Access-Control-Allow-Origin')
  if (!allowedOrigin || allowedOrigin === 'null') {
    return
  }
 
  // A cross-origin request is being allowed by the origin server or by a
  // response generated by this worker. Add the additional CORS headers
  // needed by the instrumentation script and agent
  if (allowedOrigin === '*') {
    // Access-Control-Allow-Credentials is not valid for the wildcard origin
    // but the origin is allowed by the origin server so the Origin can be
    // reflected to the response without security concerns.
    const crossOriginUrl = getCrossOriginUrl(request)
    if (crossOriginUrl) {
      originResponseHeaders.set('Access-Control-Allow-Origin', crossOriginUrl.origin)
    } else {
      // Because a wildcard origin may be set universally by some APIs that allow
      // cross-origin access from any origin, only set additional header fields when
      // the request is a cross-origin request
      return
    }
  }
 
  originResponseHeaders.set('Access-Control-Allow-Credentials', 'true')
 
  if (request.method !== 'OPTIONS') {
    if (originResponseHeaders.get(AGENT_DATA_HEADER) !== null) {
      appendHeaderValue(originResponseHeaders, 'Access-Control-Expose-Headers', AGENT_DATA_HEADER)
    }
  } else {
    appendHeaderValue(originResponseHeaders, 'Access-Control-Allow-Headers', SIGNALS_KEY)
  }
}