All files / worker/handlers handleProtectedApiOptions.ts

100% Statements 18/18
90.9% Branches 10/11
100% Functions 2/2
100% Lines 18/18

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                                        4x 4x 4x         2x 2x         1x 1x                                           3x 4x   3x       2x   2x   2x             2x   2x   2x   2x           1x    
import { SIGNALS_KEY } from '../../shared/const'
import { isSimpleMethod } from '../../shared/types'
import { TypedEnv } from '../types'
import { getAllowedOrigin } from '../urlMatching'
import { removeHeaderValue } from '../utils/headers'
import { fetchOrigin } from '../utils/origin'
import { copyRequest, setCorsHeadersForInstrumentation } from '../utils/request'
import { copyResponseWithNewHeaders } from '../utils/response'
 
export type HandleProtectedApiOptionsParams = {
  request: Request
  env: TypedEnv
}
 
export async function handleProtectedApiOptionsCall({
  request,
  env,
}: HandleProtectedApiOptionsParams): Promise<Response> {
  // Check if the OPTIONS request was caused by the addition
  // of the signals header to the instrumented request
  const accessControlRequestHeaders = request.headers.get('Access-Control-Request-Headers')
  const accessControlRequestMethod = request.headers.get('Access-Control-Request-Method')
  if (
    accessControlRequestHeaders === SIGNALS_KEY &&
    accessControlRequestMethod &&
    isSimpleMethod(accessControlRequestMethod)
  ) {
    const allowedOrigin = getAllowedOrigin(request, env)
    if (allowedOrigin) {
      // This state implies that the cross-origin request would have been a simple request
      // if not for the inclusion of the signals in the request headers. As
      // a result, the worker needs to handle this request because the origin is not
      // guaranteed to handle it.
      console.debug('Handled instrumentation-triggered preflight request without forwarding to origin')
      return new Response(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': allowedOrigin,
          'Access-Control-Allow-Headers': SIGNALS_KEY,
          'Access-Control-Allow-Methods': accessControlRequestMethod,
          'Access-Control-Allow-Credentials': 'true',
        },
      })
    }
 
    // Reaching this point indicates that the worker received a request that
    // appears to have been generated by an instrumentation page. However, the
    // origin of the request is not one of the configured identification pages.
    //
    // This likely indicates an issue with the setup such as a different Flow
    // worker instance instrumenting a page that calls a protected API handled
    // by this Flow worker.
    //
    // Attempt to fail gracefully and let the origin handle this error case.
  }
 
  const accessControlRequestHeadersValues = accessControlRequestHeaders
    ? accessControlRequestHeaders.split(',').map((s) => s.trim())
    : []
  if (accessControlRequestHeadersValues.includes(SIGNALS_KEY)) {
    // The SIGNALS_KEY needs to be removed from the forwarded request to
    // avoid unexpected results from the origin.
 
    const originRequestHeaders = new Headers(request.headers)
 
    removeHeaderValue(originRequestHeaders, 'Access-Control-Request-Headers', SIGNALS_KEY)
 
    const originRequest = copyRequest({
      request,
      init: {
        headers: originRequestHeaders,
      },
    })
 
    const originResponse = await fetchOrigin(originRequest)
 
    const originResponseHeaders = new Headers(originResponse.headers)
 
    setCorsHeadersForInstrumentation(request, originResponseHeaders)
 
    return copyResponseWithNewHeaders(originResponse, originResponseHeaders)
  }
 
  // The OPTIONS request matches a protected API but the request is not being
  // instrumented so forward the OPTIONS request to the origin. Protection
  // logic will be applied to the actual request
  return fetchOrigin(request)
}