// Loaded from https://deno.land/std@0.74.0/http/_io.ts // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { BufReader, BufWriter } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { assert } from "../_util/assert.ts"; import { encoder } from "../encoding/utf8.ts"; import { Response, ServerRequest } from "./server.ts"; import { STATUS_TEXT } from "./http_status.ts"; export function emptyReader(): Deno.Reader { return { read(_: Uint8Array): Promise { return Promise.resolve(null); }, }; } export function bodyReader(contentLength: number, r: BufReader): Deno.Reader { let totalRead = 0; let finished = false; async function read(buf: Uint8Array): Promise { if (finished) return null; let result: number | null; const remaining = contentLength - totalRead; if (remaining >= buf.byteLength) { result = await r.read(buf); } else { const readBuf = buf.subarray(0, remaining); result = await r.read(readBuf); } if (result !== null) { totalRead += result; } finished = totalRead === contentLength; return result; } return { read }; } export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 const tp = new TextProtoReader(r); let finished = false; const chunks: Array<{ offset: number; data: Uint8Array; }> = []; async function read(buf: Uint8Array): Promise { if (finished) return null; const [chunk] = chunks; if (chunk) { const chunkRemaining = chunk.data.byteLength - chunk.offset; const readLength = Math.min(chunkRemaining, buf.byteLength); for (let i = 0; i < readLength; i++) { buf[i] = chunk.data[chunk.offset + i]; } chunk.offset += readLength; if (chunk.offset === chunk.data.byteLength) { chunks.shift(); // Consume \r\n; if ((await tp.readLine()) === null) { throw new Deno.errors.UnexpectedEof(); } } return readLength; } const line = await tp.readLine(); if (line === null) throw new Deno.errors.UnexpectedEof(); // TODO: handle chunk extension const [chunkSizeString] = line.split(";"); const chunkSize = parseInt(chunkSizeString, 16); if (Number.isNaN(chunkSize) || chunkSize < 0) { throw new Error("Invalid chunk size"); } if (chunkSize > 0) { if (chunkSize > buf.byteLength) { let eof = await r.readFull(buf); if (eof === null) { throw new Deno.errors.UnexpectedEof(); } const restChunk = new Uint8Array(chunkSize - buf.byteLength); eof = await r.readFull(restChunk); if (eof === null) { throw new Deno.errors.UnexpectedEof(); } else { chunks.push({ offset: 0, data: restChunk, }); } return buf.byteLength; } else { const bufToFill = buf.subarray(0, chunkSize); const eof = await r.readFull(bufToFill); if (eof === null) { throw new Deno.errors.UnexpectedEof(); } // Consume \r\n if ((await tp.readLine()) === null) { throw new Deno.errors.UnexpectedEof(); } return chunkSize; } } else { assert(chunkSize === 0); // Consume \r\n if ((await r.readLine()) === null) { throw new Deno.errors.UnexpectedEof(); } await readTrailers(h, r); finished = true; return null; } } return { read }; } function isProhibidedForTrailer(key: string): boolean { const s = new Set(["transfer-encoding", "content-length", "trailer"]); return s.has(key.toLowerCase()); } /** Read trailer headers from reader and append values to headers. "trailer" * field will be deleted. */ export async function readTrailers( headers: Headers, r: BufReader, ): Promise { const trailers = parseTrailer(headers.get("trailer")); if (trailers == null) return; const trailerNames = [...trailers.keys()]; const tp = new TextProtoReader(r); const result = await tp.readMIMEHeader(); if (result == null) { throw new Deno.errors.InvalidData("Missing trailer header."); } const undeclared = [...result.keys()].filter( (k) => !trailerNames.includes(k), ); if (undeclared.length > 0) { throw new Deno.errors.InvalidData( `Undeclared trailers: ${Deno.inspect(undeclared)}.`, ); } for (const [k, v] of result) { headers.append(k, v); } const missingTrailers = trailerNames.filter((k) => !result.has(k)); if (missingTrailers.length > 0) { throw new Deno.errors.InvalidData( `Missing trailers: ${Deno.inspect(missingTrailers)}.`, ); } headers.delete("trailer"); } function parseTrailer(field: string | null): Headers | undefined { if (field == null) { return undefined; } const trailerNames = field.split(",").map((v) => v.trim().toLowerCase()); if (trailerNames.length === 0) { throw new Deno.errors.InvalidData("Empty trailer header."); } const prohibited = trailerNames.filter((k) => isProhibidedForTrailer(k)); if (prohibited.length > 0) { throw new Deno.errors.InvalidData( `Prohibited trailer names: ${Deno.inspect(prohibited)}.`, ); } return new Headers(trailerNames.map((key) => [key, ""])); } export async function writeChunkedBody( w: Deno.Writer, r: Deno.Reader, ): Promise { const writer = BufWriter.create(w); for await (const chunk of Deno.iter(r)) { if (chunk.byteLength <= 0) continue; const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); const end = encoder.encode("\r\n"); await writer.write(start); await writer.write(chunk); await writer.write(end); } const endChunk = encoder.encode("0\r\n\r\n"); await writer.write(endChunk); } /** Write trailer headers to writer. It should mostly should be called after * `writeResponse()`. */ export async function writeTrailers( w: Deno.Writer, headers: Headers, trailers: Headers, ): Promise { const trailer = headers.get("trailer"); if (trailer === null) { throw new TypeError("Missing trailer header."); } const transferEncoding = headers.get("transfer-encoding"); if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { throw new TypeError( `Trailers are only allowed for "transfer-encoding: chunked", got "transfer-encoding: ${transferEncoding}".`, ); } const writer = BufWriter.create(w); const trailerNames = trailer.split(",").map((s) => s.trim().toLowerCase()); const prohibitedTrailers = trailerNames.filter((k) => isProhibidedForTrailer(k) ); if (prohibitedTrailers.length > 0) { throw new TypeError( `Prohibited trailer names: ${Deno.inspect(prohibitedTrailers)}.`, ); } const undeclared = [...trailers.keys()].filter( (k) => !trailerNames.includes(k), ); if (undeclared.length > 0) { throw new TypeError(`Undeclared trailers: ${Deno.inspect(undeclared)}.`); } for (const [key, value] of trailers) { await writer.write(encoder.encode(`${key}: ${value}\r\n`)); } await writer.write(encoder.encode("\r\n")); await writer.flush(); } export async function writeResponse( w: Deno.Writer, r: Response, ): Promise { const protoMajor = 1; const protoMinor = 1; const statusCode = r.status || 200; const statusText = STATUS_TEXT.get(statusCode); const writer = BufWriter.create(w); if (!statusText) { throw new Deno.errors.InvalidData("Bad status code"); } if (!r.body) { r.body = new Uint8Array(); } if (typeof r.body === "string") { r.body = encoder.encode(r.body); } let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; const headers = r.headers ?? new Headers(); if (r.body && !headers.get("content-length")) { if (r.body instanceof Uint8Array) { out += `content-length: ${r.body.byteLength}\r\n`; } else if (!headers.get("transfer-encoding")) { out += "transfer-encoding: chunked\r\n"; } } for (const [key, value] of headers) { out += `${key}: ${value}\r\n`; } out += `\r\n`; const header = encoder.encode(out); const n = await writer.write(header); assert(n === header.byteLength); if (r.body instanceof Uint8Array) { const n = await writer.write(r.body); assert(n === r.body.byteLength); } else if (headers.has("content-length")) { const contentLength = headers.get("content-length"); assert(contentLength != null); const bodyLength = parseInt(contentLength); const n = await Deno.copy(r.body, writer); assert(n === bodyLength); } else { await writeChunkedBody(writer, r.body); } if (r.trailers) { const t = await r.trailers(); await writeTrailers(writer, headers, t); } await writer.flush(); } /** * ParseHTTPVersion parses a HTTP version string. * "HTTP/1.0" returns (1, 0). * Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request.go#L766-L792 */ export function parseHTTPVersion(vers: string): [number, number] { switch (vers) { case "HTTP/1.1": return [1, 1]; case "HTTP/1.0": return [1, 0]; default: { const Big = 1000000; // arbitrary upper bound if (!vers.startsWith("HTTP/")) { break; } const dot = vers.indexOf("."); if (dot < 0) { break; } const majorStr = vers.substring(vers.indexOf("/") + 1, dot); const major = Number(majorStr); if (!Number.isInteger(major) || major < 0 || major > Big) { break; } const minorStr = vers.substring(dot + 1); const minor = Number(minorStr); if (!Number.isInteger(minor) || minor < 0 || minor > Big) { break; } return [major, minor]; } } throw new Error(`malformed HTTP version ${vers}`); } export async function readRequest( conn: Deno.Conn, bufr: BufReader, ): Promise { const tp = new TextProtoReader(bufr); const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 if (firstLine === null) return null; const headers = await tp.readMIMEHeader(); if (headers === null) throw new Deno.errors.UnexpectedEof(); const req = new ServerRequest(); req.conn = conn; req.r = bufr; [req.method, req.url, req.proto] = firstLine.split(" ", 3); [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); req.headers = headers; fixLength(req); return req; } function fixLength(req: ServerRequest): void { const contentLength = req.headers.get("Content-Length"); if (contentLength) { const arrClen = contentLength.split(","); if (arrClen.length > 1) { const distinct = [...new Set(arrClen.map((e): string => e.trim()))]; if (distinct.length > 1) { throw Error("cannot contain multiple Content-Length headers"); } else { req.headers.set("Content-Length", distinct[0]); } } const c = req.headers.get("Content-Length"); if (req.method === "HEAD" && c && c !== "0") { throw Error("http: method cannot contain a Content-Length"); } if (c && req.headers.has("transfer-encoding")) { // A sender MUST NOT send a Content-Length header field in any message // that contains a Transfer-Encoding header field. // rfc: https://tools.ietf.org/html/rfc7230#section-3.3.2 throw new Error( "http: Transfer-Encoding and Content-Length cannot be send together", ); } } }