| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 |
- /** Bounded raw HTTP body intake for GitHub signature verification. */
- import type { IncomingMessage } from 'node:http'
- /** HTTP refusal whose message is safe to return without request data. */
- export class WebhookHttpError extends Error {
- override readonly name = 'WebhookHttpError'
- constructor(
- readonly status: 400 | 401 | 405 | 413 | 415 | 503,
- message: string,
- ) {
- super(message)
- }
- }
- /** Parse a decimal Content-Length or reject an ambiguous header. */
- function contentLength(request: IncomingMessage): number | undefined {
- const value = request.headers['content-length']
- if (value === undefined) return undefined
- if (!/^(0|[1-9]\d*)$/.test(value)) {
- throw new WebhookHttpError(400, 'invalid Content-Length')
- }
- const length = Number(value)
- if (!Number.isSafeInteger(length)) throw new WebhookHttpError(413, 'request body is too large')
- return length
- }
- /**
- * Read one request body as exact, bounded UTF-8 text.
- * @param request - incoming request before any parser consumes it.
- * @param maxBodyBytes - positive byte ceiling.
- * @returns the decoded body after EOF.
- * @throws {WebhookHttpError} for invalid length, excessive bytes, invalid UTF-8, or an aborted stream.
- */
- export async function readBoundedUtf8Body(
- request: IncomingMessage,
- maxBodyBytes: number,
- ): Promise<string> {
- const declared = contentLength(request)
- if (declared !== undefined && declared > maxBodyBytes) {
- request.resume()
- throw new WebhookHttpError(413, 'request body is too large')
- }
- const chunks: Buffer[] = []
- let size = 0
- try {
- for await (const raw of request) {
- const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw as string)
- size += chunk.byteLength
- if (size > maxBodyBytes) {
- request.resume()
- throw new WebhookHttpError(413, 'request body is too large')
- }
- chunks.push(chunk)
- }
- } catch (error: unknown) {
- if (error instanceof WebhookHttpError) throw error
- throw new WebhookHttpError(400, 'request body was aborted')
- }
- if (!request.complete) throw new WebhookHttpError(400, 'request body was aborted')
- try {
- return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks, size))
- } catch {
- // TextDecoder is the only statement in the try; GitHub JSON must be valid UTF-8.
- throw new WebhookHttpError(400, 'request body is not valid UTF-8')
- }
- }
|