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.ts175
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"
+ );
+ }
+ }
+}