{"version":3,"file":"handler.cjs","names":["HeaderName","HeaderValue","isGetOrHead","StatusCode","getFilenameFromUrl","createETag","Options","parseRange","isGet","isHead"],"sources":["../src/handler.ts"],"sourcesContent":["import type { IncomingMessage, RequestListener, ServerResponse } from \"node:http\";\nimport { pipeline } from \"node:stream/promises\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport type { PFrameInternal } from \"@milaboratories/pl-model-middle-layer\";\nimport {\n  createETag,\n  getFilenameFromUrl,\n  parseRange,\n  isGetOrHead,\n  isGet,\n  isHead,\n  Options,\n  StatusCode,\n  HeaderName,\n  HeaderValue,\n} from \"./utils\";\nimport { isAbortError } from \"@milaboratories/pl-model-common\";\n\n/** Main request handler for parquet files */\nfunction handleRequest(\n  request: IncomingMessage,\n  response: ServerResponse,\n  store: PFrameInternal.ObjectStore,\n): void {\n  // RFC 9110 section 6.6.1: Date header should be present in all responses\n  response.sendDate = true;\n  // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n  response.strictContentLength = true;\n  response.setHeader(HeaderName.ContentLength, 0);\n  // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n  // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n  response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n\n  // RFC 9110 section 15.5.6: Method not allowed\n  const method = request.method;\n  if (!isGetOrHead(method)) {\n    response.setHeader(HeaderName.Allow, HeaderValue.Allow);\n    return void response.writeHead(StatusCode.MethodNotAllowed).end();\n  }\n\n  const filename = getFilenameFromUrl(request);\n  if (filename === null) {\n    return void response.writeHead(StatusCode.Gone).end();\n  }\n\n  // From now on we are sure that the response would be a Parquet file\n  response.setHeader(HeaderName.AcceptRanges, HeaderValue.AcceptRanges);\n  response.setHeader(HeaderName.ContentType, HeaderValue.ContentType);\n\n  // RFC 9110 section 8.8.3: ETag header is used for cache versioning\n  const etag = createETag(filename);\n  // RFC 9110 section 8.8.2: Last-Modified header field for cache validation\n  const mtime = new Date(0); // Using fake fixed date since files are immutable\n  // RFC 9111 section 5.2: Cache-Control header with public allows to cache authenticated responses\n  response.setHeader(HeaderName.CacheControl, HeaderValue.CacheControl);\n  response.setHeader(HeaderName.ETag, etag);\n  response.setHeader(HeaderName.LastModified, mtime.toUTCString());\n\n  const options = new Options(request);\n  // RFC 9110 section 13.1.1: If-Match precondition evaluation\n  // RFC 9110 section 13.1.4: If-Unmodified-Since precondition evaluation\n  if (options.preconditionFailed(etag, mtime)) {\n    return void response.writeHead(StatusCode.PreconditionFailed).end();\n  }\n  // RFC 9110 section 13.1.2: If-None-Match precondition evaluation\n  // RFC 9110 section 13.1.3: If-Modified-Since precondition evaluation\n  else if (options.notModified(etag, mtime)) {\n    return void response.writeHead(StatusCode.NotModified).end();\n  }\n\n  const range = parseRange(request);\n  if (range === null) {\n    return void response.writeHead(StatusCode.BadRequest).end();\n  }\n\n  const abortController = new AbortController();\n  request.on(\"close\", () => abortController.abort());\n  const signal = abortController.signal;\n\n  store.request(filename, {\n    method,\n    range,\n    signal,\n    // pipeline automatically destroys the streams if they were not gracefully closed\n    callback: async (result) => {\n      if (request.destroyed) {\n        // request has timed out, close the connection\n        if (response.destroyed) {\n          return;\n        } else if (response.headersSent) {\n          return void response.end();\n        } else {\n          response.setHeader(HeaderName.Connection, HeaderValue.Connection);\n          return void response.writeHead(StatusCode.RequestTimeout).end();\n        }\n      }\n\n      switch (result.type) {\n        case \"InternalError\":\n          // object store encountered network error, retry by client can help\n          return void response.writeHead(StatusCode.InternalServerError).end();\n        case \"NotFound\":\n          // RFC 9110 section 15.4.5: Not found\n          return void response.writeHead(StatusCode.NotFound).end();\n        case \"RangeNotSatisfiable\":\n          // RFC 9110 section 15.5.17: Range not satisfiable\n          response.setHeader(HeaderName.ContentRange, `bytes */${result.size}`);\n          return void response.writeHead(StatusCode.RangeNotSatisfiable).end();\n        case \"Ok\":\n          break;\n      }\n\n      if (isGet(method) && !result.data) {\n        // object store implementation is incorrect, retry by client cannot help\n        return void response.writeHead(StatusCode.GatewayTimeout).end();\n      }\n\n      if (range) {\n        // RFC 9110 section 14.4: Partial content response\n        response.setHeader(HeaderName.ContentLength, result.range.end - result.range.start + 1);\n        response.setHeader(\n          HeaderName.ContentRange,\n          `bytes ${result.range.start}-${result.range.end}/${result.size}`,\n        );\n        response.writeHead(StatusCode.PartialContent);\n      } else {\n        // RFC 9110 section 15.3.1: OK response\n        response.setHeader(HeaderName.ContentLength, result.size);\n        response.writeHead(StatusCode.Ok);\n      }\n\n      // RFC 9110 section 9.3.2: HEAD method must not return message body\n      if (isHead(method)) {\n        return void response.end();\n      }\n\n      try {\n        return await pipeline(result.data!, response, { signal });\n      } catch (error: unknown) {\n        if (!isAbortError(error)) throw error;\n      }\n    },\n  });\n}\n\n/**\n * Create a request handler for serving files from an object store\n * compatible with HTTP/1.1 as defined in RFC 9110 and RFC 9111:\n * - <https://datatracker.ietf.org/doc/html/rfc9110>\n * - <https://datatracker.ietf.org/doc/html/rfc9111>\n *\n * Accepts only paths of the form `/<filename>.parquet`, returns 410 Gone otherwise\n * Assumes that files are immutable (and sets cache headers accordingly)\n */\nexport function createRequestHandler(\n  options: PFrameInternal.RequestHandlerOptions,\n): RequestListener {\n  const { store } = options;\n  return (request, response) => handleRequest(request, response, store);\n}\n\n/** Request authorization middleware */\nfunction authorizeRequest(\n  request: IncomingMessage,\n  response: ServerResponse,\n  handler: RequestListener,\n  authHeader: string,\n): void {\n  // RFC 9110 section 6.6.1: Date header should be present in all responses\n  response.sendDate = true;\n  // RFC 9110 section 8.6: Content-Length 0 as default for error responses\n  response.strictContentLength = true;\n  response.setHeader(HeaderName.ContentLength, 0);\n  // Note: setting Content-Length disables Node.js default Transfer-Encoding: chunked\n\n  const actualHeader = request.headers[HeaderName.Authorization];\n\n  // Early length check to avoid unnecessary processing\n  if (!actualHeader || actualHeader.length !== authHeader.length) {\n    // RFC 9110 section 11.6.1: WWW-Authenticate header field\n    response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n    return void response.writeHead(StatusCode.Unauthorized).end();\n  }\n\n  // Use timing-safe comparison to prevent timing attacks\n  // <https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/>\n  const encoder = new TextEncoder();\n  const receivedBuffer = encoder.encode(actualHeader);\n  const expectedBuffer = encoder.encode(authHeader);\n\n  if (\n    receivedBuffer.byteLength !== expectedBuffer.byteLength ||\n    !timingSafeEqual(receivedBuffer, expectedBuffer)\n  ) {\n    response.setHeader(HeaderName.WWWAuthenticate, HeaderValue.WWWAuthenticate);\n    return void response.writeHead(StatusCode.Unauthorized).end();\n  }\n\n  return handler(request, response);\n}\n\n/** Apply Bearer token authorization to @param handler */\nexport function authorizeRequestHandler(\n  handler: RequestListener,\n  authToken: PFrameInternal.HttpAuthorizationToken,\n): RequestListener {\n  const authHeader = `Bearer ${authToken}`;\n  return (request, response) => authorizeRequest(request, response, handler, authHeader);\n}\n"],"mappings":"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"}