summaryrefslogtreecommitdiff
path: root/std/http/_io.ts
diff options
context:
space:
mode:
Diffstat (limited to 'std/http/_io.ts')
-rw-r--r--std/http/_io.ts380
1 files changed, 0 insertions, 380 deletions
diff --git a/std/http/_io.ts b/std/http/_io.ts
deleted file mode 100644
index 529f59cb5..000000000
--- a/std/http/_io.ts
+++ /dev/null
@@ -1,380 +0,0 @@
-// Copyright 2018-2021 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<number | null> {
- 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<number | null> {
- 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<number | null> {
- 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(bartlomieju): handle chunk extension
- const [chunkSizeString] = line.split(";");
- const chunkSize = parseInt(chunkSizeString, 16);
- if (Number.isNaN(chunkSize) || chunkSize < 0) {
- throw new Deno.errors.InvalidData("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<void> {
- 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: BufWriter,
- r: Deno.Reader,
-): Promise<void> {
- 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 w.write(start);
- await w.write(chunk);
- await w.write(end);
- await w.flush();
- }
-
- const endChunk = encoder.encode("0\r\n\r\n");
- await w.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<void> {
- 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<void> {
- 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<ServerRequest | null> {
- 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.protoMajor, req.protoMinor] = 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",
- );
- }
- }
-}