diff options
author | Yusuke Sakurai <kerokerokerop@gmail.com> | 2020-02-27 00:48:35 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-26 10:48:35 -0500 |
commit | 942e67c00b8ebdf6671fc8bb2e6c78c3ad8b3ff8 (patch) | |
tree | 9e53511a0cd6d55d4e9eb96e58f9b54e481981a5 /std/http/io.ts | |
parent | 9a8d6fbd984d2c0f9775d90af64b3e95231dd21d (diff) |
refactor(std/http): move io functions to http/io.ts (#4126)
Diffstat (limited to 'std/http/io.ts')
-rw-r--r-- | std/http/io.ts | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/std/http/io.ts b/std/http/io.ts index a51fada54..477bab831 100644 --- a/std/http/io.ts +++ b/std/http/io.ts @@ -2,6 +2,8 @@ import { BufReader, UnexpectedEOFError, BufWriter } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { assert } from "../testing/asserts.ts"; import { encoder } from "../strings/mod.ts"; +import { ServerRequest, Response } from "./server.ts"; +import { STATUS_TEXT } from "./http_status.ts"; export function emptyReader(): Deno.Reader { return { @@ -211,3 +213,176 @@ export async function writeTrailers( await writer.write(encoder.encode("\r\n")); await writer.flush(); } + +export function setContentLength(r: Response): void { + if (!r.headers) { + r.headers = new Headers(); + } + + if (r.body) { + if (!r.headers.has("content-length")) { + // typeof r.body === "string" handled in writeResponse. + if (r.body instanceof Uint8Array) { + const bodyLength = r.body.byteLength; + r.headers.set("content-length", bodyLength.toString()); + } else { + r.headers.set("transfer-encoding", "chunked"); + } + } + } +} + +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 Error("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`; + + setContentLength(r); + assert(r.headers != null); + const headers = r.headers; + + 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(writer, r.body); + 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, true). + * 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 + const digitReg = /^\d+$/; // test if string is only digit + + if (!vers.startsWith("HTTP/")) { + break; + } + + const dot = vers.indexOf("."); + if (dot < 0) { + break; + } + + const majorStr = vers.substring(vers.indexOf("/") + 1, dot); + const major = parseInt(majorStr); + if ( + !digitReg.test(majorStr) || + isNaN(major) || + major < 0 || + major > Big + ) { + break; + } + + const minorStr = vers.substring(dot + 1); + const minor = parseInt(minorStr); + if ( + !digitReg.test(minorStr) || + isNaN(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 | Deno.EOF> { + const tp = new TextProtoReader(bufr); + const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 + if (firstLine === Deno.EOF) return Deno.EOF; + const headers = await tp.readMIMEHeader(); + if (headers === Deno.EOF) throw new UnexpectedEOFError(); + + 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" + ); + } + } +} |