summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
Diffstat (limited to 'http')
-rw-r--r--http/README.md26
-rwxr-xr-xhttp/file_server.ts241
-rw-r--r--http/file_server_test.ts51
-rw-r--r--http/http.ts325
-rw-r--r--http/http_bench.ts15
-rw-r--r--http/http_status.ts134
-rw-r--r--http/http_test.ts223
-rw-r--r--http/mod.ts8
8 files changed, 1023 insertions, 0 deletions
diff --git a/http/README.md b/http/README.md
new file mode 100644
index 000000000..e81e42a41
--- /dev/null
+++ b/http/README.md
@@ -0,0 +1,26 @@
+# net
+
+Usage:
+
+```typescript
+import { serve } from "https://deno.land/x/http/mod.ts";
+const s = serve("0.0.0.0:8000");
+
+async function main() {
+ for await (const req of s) {
+ req.respond({ body: new TextEncoder().encode("Hello World\n") });
+ }
+}
+
+main();
+```
+
+### File Server
+
+A small program for serving local files over HTTP.
+
+Add the following to your `.bash_profile`
+
+```
+alias file_server="deno https://deno.land/x/http/file_server.ts --allow-net"
+```
diff --git a/http/file_server.ts b/http/file_server.ts
new file mode 100755
index 000000000..4437a44e4
--- /dev/null
+++ b/http/file_server.ts
@@ -0,0 +1,241 @@
+#!/usr/bin/env deno --allow-net
+
+// This program serves files in the current directory over HTTP.
+// TODO Stream responses instead of reading them into memory.
+// TODO Add tests like these:
+// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js
+
+import {
+ listenAndServe,
+ ServerRequest,
+ setContentLength,
+ Response
+} from "./mod.ts";
+import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno";
+import { extname } from "../fs/path.ts";
+import { contentType } from "../media_types/mod.ts";
+
+const dirViewerTemplate = `
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <title>Deno File Server</title>
+ <style>
+ td {
+ padding: 0 1rem;
+ }
+ td.mode {
+ font-family: Courier;
+ }
+ </style>
+</head>
+<body>
+ <h1>Index of <%DIRNAME%></h1>
+ <table>
+ <tr><th>Mode</th><th>Size</th><th>Name</th></tr>
+ <%CONTENTS%>
+ </table>
+</body>
+</html>
+`;
+
+const serverArgs = args.slice();
+let CORSEnabled = false;
+// TODO: switch to flags if we later want to add more options
+for (let i = 0; i < serverArgs.length; i++) {
+ if (serverArgs[i] === "--cors") {
+ CORSEnabled = true;
+ serverArgs.splice(i, 1);
+ break;
+ }
+}
+let currentDir = cwd();
+const target = serverArgs[1];
+if (target) {
+ currentDir = `${currentDir}/${target}`;
+}
+const addr = `0.0.0.0:${serverArgs[2] || 4500}`;
+const encoder = new TextEncoder();
+
+function modeToString(isDir: boolean, maybeMode: number | null) {
+ const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"];
+
+ if (maybeMode === null) {
+ return "(unknown mode)";
+ }
+ const mode = maybeMode!.toString(8);
+ if (mode.length < 3) {
+ return "(unknown mode)";
+ }
+ let output = "";
+ mode
+ .split("")
+ .reverse()
+ .slice(0, 3)
+ .forEach(v => {
+ output = modeMap[+v] + output;
+ });
+ output = `(${isDir ? "d" : "-"}${output})`;
+ return output;
+}
+
+function fileLenToString(len: number) {
+ const multipler = 1024;
+ let base = 1;
+ const suffix = ["B", "K", "M", "G", "T"];
+ let suffixIndex = 0;
+
+ while (base * multipler < len) {
+ if (suffixIndex >= suffix.length - 1) {
+ break;
+ }
+ base *= multipler;
+ suffixIndex++;
+ }
+
+ return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`;
+}
+
+function createDirEntryDisplay(
+ name: string,
+ path: string,
+ size: number | null,
+ mode: number | null,
+ isDir: boolean
+) {
+ const sizeStr = size === null ? "" : "" + fileLenToString(size!);
+ return `
+ <tr><td class="mode">${modeToString(
+ isDir,
+ mode
+ )}</td><td>${sizeStr}</td><td><a href="${path}">${name}${
+ isDir ? "/" : ""
+ }</a></td>
+ </tr>
+ `;
+}
+
+// TODO: simplify this after deno.stat and deno.readDir are fixed
+async function serveDir(req: ServerRequest, dirPath: string, dirName: string) {
+ // dirname has no prefix
+ const listEntry: string[] = [];
+ const fileInfos = await readDir(dirPath);
+ for (const info of fileInfos) {
+ if (info.name === "index.html" && info.isFile()) {
+ // in case index.html as dir...
+ return await serveFile(req, info.path);
+ }
+ // Yuck!
+ let mode = null;
+ try {
+ mode = (await stat(info.path)).mode;
+ } catch (e) {}
+ listEntry.push(
+ createDirEntryDisplay(
+ info.name,
+ dirName + "/" + info.name,
+ info.isFile() ? info.len : null,
+ mode,
+ info.isDirectory()
+ )
+ );
+ }
+
+ const page = new TextEncoder().encode(
+ dirViewerTemplate
+ .replace("<%DIRNAME%>", dirName + "/")
+ .replace("<%CONTENTS%>", listEntry.join(""))
+ );
+
+ const headers = new Headers();
+ headers.set("content-type", "text/html");
+
+ const res = {
+ status: 200,
+ body: page,
+ headers
+ };
+ setContentLength(res);
+ return res;
+}
+
+async function serveFile(req: ServerRequest, filename: string) {
+ const file = await open(filename);
+ const fileInfo = await stat(filename);
+ const headers = new Headers();
+ headers.set("content-length", fileInfo.len.toString());
+ headers.set("content-type", contentType(extname(filename)) || "text/plain");
+
+ const res = {
+ status: 200,
+ body: file,
+ headers
+ };
+ return res;
+}
+
+async function serveFallback(req: ServerRequest, e: Error) {
+ if (
+ e instanceof DenoError &&
+ (e as DenoError<any>).kind === ErrorKind.NotFound
+ ) {
+ return {
+ status: 404,
+ body: encoder.encode("Not found")
+ };
+ } else {
+ return {
+ status: 500,
+ body: encoder.encode("Internal server error")
+ };
+ }
+}
+
+function serverLog(req: ServerRequest, res: Response) {
+ const d = new Date().toISOString();
+ const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`;
+ const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`;
+ console.log(s);
+}
+
+function setCORS(res: Response) {
+ if (!res.headers) {
+ res.headers = new Headers();
+ }
+ res.headers!.append("access-control-allow-origin", "*");
+ res.headers!.append(
+ "access-control-allow-headers",
+ "Origin, X-Requested-With, Content-Type, Accept, Range"
+ );
+}
+
+listenAndServe(addr, async req => {
+ const fileName = req.url.replace(/\/$/, "");
+ const filePath = currentDir + fileName;
+
+ let response: Response;
+
+ try {
+ const fileInfo = await stat(filePath);
+ if (fileInfo.isDirectory()) {
+ // Bug with deno.stat: name and path not populated
+ // Yuck!
+ response = await serveDir(req, filePath, fileName);
+ } else {
+ response = await serveFile(req, filePath);
+ }
+ } catch (e) {
+ response = await serveFallback(req, e);
+ } finally {
+ if (CORSEnabled) {
+ setCORS(response);
+ }
+ serverLog(req, response);
+ req.respond(response);
+ }
+});
+
+console.log(`HTTP server listening on http://${addr}/`);
diff --git a/http/file_server_test.ts b/http/file_server_test.ts
new file mode 100644
index 000000000..bd00d749b
--- /dev/null
+++ b/http/file_server_test.ts
@@ -0,0 +1,51 @@
+import { readFile } from "deno";
+
+import { test, assert, assertEqual } from "../testing/mod.ts";
+
+// Promise to completeResolve when all tests completes
+let completeResolve;
+export const completePromise = new Promise(res => (completeResolve = res));
+let completedTestCount = 0;
+
+function maybeCompleteTests() {
+ completedTestCount++;
+ // Change this when adding more tests
+ if (completedTestCount === 3) {
+ completeResolve();
+ }
+}
+
+export function runTests(serverReadyPromise: Promise<any>) {
+ test(async function serveFile() {
+ await serverReadyPromise;
+ const res = await fetch("http://localhost:4500/azure-pipelines.yml");
+ assert(res.headers.has("access-control-allow-origin"));
+ assert(res.headers.has("access-control-allow-headers"));
+ assertEqual(res.headers.get("content-type"), "text/yaml; charset=utf-8");
+ const downloadedFile = await res.text();
+ const localFile = new TextDecoder().decode(
+ await readFile("./azure-pipelines.yml")
+ );
+ assertEqual(downloadedFile, localFile);
+ maybeCompleteTests();
+ });
+
+ test(async function serveDirectory() {
+ await serverReadyPromise;
+ const res = await fetch("http://localhost:4500/");
+ assert(res.headers.has("access-control-allow-origin"));
+ assert(res.headers.has("access-control-allow-headers"));
+ const page = await res.text();
+ assert(page.includes("azure-pipelines.yml"));
+ maybeCompleteTests();
+ });
+
+ test(async function serveFallback() {
+ await serverReadyPromise;
+ const res = await fetch("http://localhost:4500/badfile.txt");
+ assert(res.headers.has("access-control-allow-origin"));
+ assert(res.headers.has("access-control-allow-headers"));
+ assertEqual(res.status, 404);
+ maybeCompleteTests();
+ });
+}
diff --git a/http/http.ts b/http/http.ts
new file mode 100644
index 000000000..da7bc0169
--- /dev/null
+++ b/http/http.ts
@@ -0,0 +1,325 @@
+import { listen, Conn, toAsyncIterator, Reader, copy } from "deno";
+import { BufReader, BufState, BufWriter } from "../io/bufio.ts";
+import { TextProtoReader } from "../textproto/mod.ts";
+import { STATUS_TEXT } from "./http_status.ts";
+import { assert } from "../io/util.ts";
+
+interface Deferred {
+ promise: Promise<{}>;
+ resolve: () => void;
+ reject: () => void;
+}
+
+function deferred(): Deferred {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ return {
+ promise,
+ resolve,
+ reject
+ };
+}
+
+interface ServeEnv {
+ reqQueue: ServerRequest[];
+ serveDeferred: Deferred;
+}
+
+/** Continuously read more requests from conn until EOF
+ * Calls maybeHandleReq.
+ * bufr is empty on a fresh TCP connection.
+ * Would be passed around and reused for later request on same conn
+ * TODO: make them async function after this change is done
+ * https://github.com/tc39/ecma262/pull/1250
+ * See https://v8.dev/blog/fast-async
+ */
+function serveConn(env: ServeEnv, conn: Conn, bufr?: BufReader) {
+ readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn));
+}
+function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) {
+ const [req, _err] = maybeReq;
+ if (_err) {
+ conn.close(); // assume EOF for now...
+ return;
+ }
+ env.reqQueue.push(req); // push req to queue
+ env.serveDeferred.resolve(); // signal while loop to process it
+}
+
+export async function* serve(addr: string) {
+ const listener = listen("tcp", addr);
+ const env: ServeEnv = {
+ reqQueue: [], // in case multiple promises are ready
+ serveDeferred: deferred()
+ };
+
+ // Routine that keeps calling accept
+ const acceptRoutine = () => {
+ const handleConn = (conn: Conn) => {
+ serveConn(env, conn); // don't block
+ scheduleAccept(); // schedule next accept
+ };
+ const scheduleAccept = () => {
+ listener.accept().then(handleConn);
+ };
+ scheduleAccept();
+ };
+
+ acceptRoutine();
+
+ // Loop hack to allow yield (yield won't work in callbacks)
+ while (true) {
+ await env.serveDeferred.promise;
+ env.serveDeferred = deferred(); // use a new deferred
+ let queueToProcess = env.reqQueue;
+ env.reqQueue = [];
+ for (const result of queueToProcess) {
+ yield result;
+ // Continue read more from conn when user is done with the current req
+ // Moving this here makes it easier to manage
+ serveConn(env, result.conn, result.r);
+ }
+ }
+ listener.close();
+}
+
+export async function listenAndServe(
+ addr: string,
+ handler: (req: ServerRequest) => void
+) {
+ const server = serve(addr);
+
+ for await (const request of server) {
+ await handler(request);
+ }
+}
+
+export interface Response {
+ status?: number;
+ headers?: Headers;
+ body?: Uint8Array | Reader;
+}
+
+export function setContentLength(r: Response): void {
+ if (!r.headers) {
+ r.headers = new Headers();
+ }
+
+ if (r.body) {
+ if (!r.headers.has("content-length")) {
+ if (r.body instanceof Uint8Array) {
+ const bodyLength = r.body.byteLength;
+ r.headers.append("Content-Length", bodyLength.toString());
+ } else {
+ r.headers.append("Transfer-Encoding", "chunked");
+ }
+ }
+ }
+}
+
+export class ServerRequest {
+ url: string;
+ method: string;
+ proto: string;
+ headers: Headers;
+ conn: Conn;
+ r: BufReader;
+ w: BufWriter;
+
+ public async *bodyStream() {
+ if (this.headers.has("content-length")) {
+ const len = +this.headers.get("content-length");
+ if (Number.isNaN(len)) {
+ return new Uint8Array(0);
+ }
+ let buf = new Uint8Array(1024);
+ let rr = await this.r.read(buf);
+ let nread = rr.nread;
+ while (!rr.eof && nread < len) {
+ yield buf.subarray(0, rr.nread);
+ buf = new Uint8Array(1024);
+ rr = await this.r.read(buf);
+ nread += rr.nread;
+ }
+ yield buf.subarray(0, rr.nread);
+ } else {
+ if (this.headers.has("transfer-encoding")) {
+ const transferEncodings = this.headers
+ .get("transfer-encoding")
+ .split(",")
+ .map(e => e.trim().toLowerCase());
+ if (transferEncodings.includes("chunked")) {
+ // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
+ const tp = new TextProtoReader(this.r);
+ let [line, _] = await tp.readLine();
+ // TODO: handle chunk extension
+ let [chunkSizeString, optExt] = line.split(";");
+ let chunkSize = parseInt(chunkSizeString, 16);
+ if (Number.isNaN(chunkSize) || chunkSize < 0) {
+ throw new Error("Invalid chunk size");
+ }
+ while (chunkSize > 0) {
+ let data = new Uint8Array(chunkSize);
+ let [nread, err] = await this.r.readFull(data);
+ if (nread !== chunkSize) {
+ throw new Error("Chunk data does not match size");
+ }
+ yield data;
+ await this.r.readLine(); // Consume \r\n
+ [line, _] = await tp.readLine();
+ chunkSize = parseInt(line, 16);
+ }
+ const [entityHeaders, err] = await tp.readMIMEHeader();
+ if (!err) {
+ for (let [k, v] of entityHeaders) {
+ this.headers.set(k, v);
+ }
+ }
+ /* Pseudo code from https://tools.ietf.org/html/rfc2616#section-19.4.6
+ length := 0
+ read chunk-size, chunk-extension (if any) and CRLF
+ while (chunk-size > 0) {
+ read chunk-data and CRLF
+ append chunk-data to entity-body
+ length := length + chunk-size
+ read chunk-size and CRLF
+ }
+ read entity-header
+ while (entity-header not empty) {
+ append entity-header to existing header fields
+ read entity-header
+ }
+ Content-Length := length
+ Remove "chunked" from Transfer-Encoding
+ */
+ return; // Must return here to avoid fall through
+ }
+ // TODO: handle other transfer-encoding types
+ }
+ // Otherwise...
+ yield new Uint8Array(0);
+ }
+ }
+
+ // Read the body of the request into a single Uint8Array
+ public async body(): Promise<Uint8Array> {
+ return readAllIterator(this.bodyStream());
+ }
+
+ private async _streamBody(body: Reader, bodyLength: number) {
+ const n = await copy(this.w, body);
+ assert(n == bodyLength);
+ }
+
+ private async _streamChunkedBody(body: Reader) {
+ const encoder = new TextEncoder();
+
+ for await (const chunk of toAsyncIterator(body)) {
+ const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`);
+ const end = encoder.encode("\r\n");
+ await this.w.write(start);
+ await this.w.write(chunk);
+ await this.w.write(end);
+ }
+
+ const endChunk = encoder.encode("0\r\n\r\n");
+ await this.w.write(endChunk);
+ }
+
+ async respond(r: Response): Promise<void> {
+ const protoMajor = 1;
+ const protoMinor = 1;
+ const statusCode = r.status || 200;
+ const statusText = STATUS_TEXT.get(statusCode);
+ if (!statusText) {
+ throw Error("bad status code");
+ }
+
+ let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`;
+
+ setContentLength(r);
+
+ if (r.headers) {
+ for (const [key, value] of r.headers) {
+ out += `${key}: ${value}\r\n`;
+ }
+ }
+ out += "\r\n";
+
+ const header = new TextEncoder().encode(out);
+ let n = await this.w.write(header);
+ assert(header.byteLength == n);
+
+ if (r.body) {
+ if (r.body instanceof Uint8Array) {
+ n = await this.w.write(r.body);
+ assert(r.body.byteLength == n);
+ } else {
+ if (r.headers.has("content-length")) {
+ await this._streamBody(
+ r.body,
+ parseInt(r.headers.get("content-length"))
+ );
+ } else {
+ await this._streamChunkedBody(r.body);
+ }
+ }
+ }
+
+ await this.w.flush();
+ }
+}
+
+async function readRequest(
+ c: Conn,
+ bufr?: BufReader
+): Promise<[ServerRequest, BufState]> {
+ if (!bufr) {
+ bufr = new BufReader(c);
+ }
+ const bufw = new BufWriter(c);
+ const req = new ServerRequest();
+ req.conn = c;
+ req.r = bufr!;
+ req.w = bufw;
+ const tp = new TextProtoReader(bufr!);
+
+ let s: string;
+ let err: BufState;
+
+ // First line: GET /index.html HTTP/1.0
+ [s, err] = await tp.readLine();
+ if (err) {
+ return [null, err];
+ }
+ [req.method, req.url, req.proto] = s.split(" ", 3);
+
+ [req.headers, err] = await tp.readMIMEHeader();
+
+ return [req, err];
+}
+
+async function readAllIterator(
+ it: AsyncIterableIterator<Uint8Array>
+): Promise<Uint8Array> {
+ const chunks = [];
+ let len = 0;
+ for await (const chunk of it) {
+ chunks.push(chunk);
+ len += chunk.length;
+ }
+ if (chunks.length === 0) {
+ // No need for copy
+ return chunks[0];
+ }
+ const collected = new Uint8Array(len);
+ let offset = 0;
+ for (let chunk of chunks) {
+ collected.set(chunk, offset);
+ offset += chunk.length;
+ }
+ return collected;
+}
diff --git a/http/http_bench.ts b/http/http_bench.ts
new file mode 100644
index 000000000..5aca12f55
--- /dev/null
+++ b/http/http_bench.ts
@@ -0,0 +1,15 @@
+import * as deno from "deno";
+import { serve } from "./mod.ts";
+
+const addr = deno.args[1] || "127.0.0.1:4500";
+const server = serve(addr);
+
+const body = new TextEncoder().encode("Hello World");
+
+async function main(): Promise<void> {
+ for await (const request of server) {
+ await request.respond({ status: 200, body });
+ }
+}
+
+main();
diff --git a/http/http_status.ts b/http/http_status.ts
new file mode 100644
index 000000000..a3006d319
--- /dev/null
+++ b/http/http_status.ts
@@ -0,0 +1,134 @@
+export enum Status {
+ Continue = 100, // RFC 7231, 6.2.1
+ SwitchingProtocols = 101, // RFC 7231, 6.2.2
+ Processing = 102, // RFC 2518, 10.1
+
+ OK = 200, // RFC 7231, 6.3.1
+ Created = 201, // RFC 7231, 6.3.2
+ Accepted = 202, // RFC 7231, 6.3.3
+ NonAuthoritativeInfo = 203, // RFC 7231, 6.3.4
+ NoContent = 204, // RFC 7231, 6.3.5
+ ResetContent = 205, // RFC 7231, 6.3.6
+ PartialContent = 206, // RFC 7233, 4.1
+ MultiStatus = 207, // RFC 4918, 11.1
+ AlreadyReported = 208, // RFC 5842, 7.1
+ IMUsed = 226, // RFC 3229, 10.4.1
+
+ MultipleChoices = 300, // RFC 7231, 6.4.1
+ MovedPermanently = 301, // RFC 7231, 6.4.2
+ Found = 302, // RFC 7231, 6.4.3
+ SeeOther = 303, // RFC 7231, 6.4.4
+ NotModified = 304, // RFC 7232, 4.1
+ UseProxy = 305, // RFC 7231, 6.4.5
+ // _ = 306, // RFC 7231, 6.4.6 (Unused)
+ TemporaryRedirect = 307, // RFC 7231, 6.4.7
+ PermanentRedirect = 308, // RFC 7538, 3
+
+ BadRequest = 400, // RFC 7231, 6.5.1
+ Unauthorized = 401, // RFC 7235, 3.1
+ PaymentRequired = 402, // RFC 7231, 6.5.2
+ Forbidden = 403, // RFC 7231, 6.5.3
+ NotFound = 404, // RFC 7231, 6.5.4
+ MethodNotAllowed = 405, // RFC 7231, 6.5.5
+ NotAcceptable = 406, // RFC 7231, 6.5.6
+ ProxyAuthRequired = 407, // RFC 7235, 3.2
+ RequestTimeout = 408, // RFC 7231, 6.5.7
+ Conflict = 409, // RFC 7231, 6.5.8
+ Gone = 410, // RFC 7231, 6.5.9
+ LengthRequired = 411, // RFC 7231, 6.5.10
+ PreconditionFailed = 412, // RFC 7232, 4.2
+ RequestEntityTooLarge = 413, // RFC 7231, 6.5.11
+ RequestURITooLong = 414, // RFC 7231, 6.5.12
+ UnsupportedMediaType = 415, // RFC 7231, 6.5.13
+ RequestedRangeNotSatisfiable = 416, // RFC 7233, 4.4
+ ExpectationFailed = 417, // RFC 7231, 6.5.14
+ Teapot = 418, // RFC 7168, 2.3.3
+ MisdirectedRequest = 421, // RFC 7540, 9.1.2
+ UnprocessableEntity = 422, // RFC 4918, 11.2
+ Locked = 423, // RFC 4918, 11.3
+ FailedDependency = 424, // RFC 4918, 11.4
+ UpgradeRequired = 426, // RFC 7231, 6.5.15
+ PreconditionRequired = 428, // RFC 6585, 3
+ TooManyRequests = 429, // RFC 6585, 4
+ RequestHeaderFieldsTooLarge = 431, // RFC 6585, 5
+ UnavailableForLegalReasons = 451, // RFC 7725, 3
+
+ InternalServerError = 500, // RFC 7231, 6.6.1
+ NotImplemented = 501, // RFC 7231, 6.6.2
+ BadGateway = 502, // RFC 7231, 6.6.3
+ ServiceUnavailable = 503, // RFC 7231, 6.6.4
+ GatewayTimeout = 504, // RFC 7231, 6.6.5
+ HTTPVersionNotSupported = 505, // RFC 7231, 6.6.6
+ VariantAlsoNegotiates = 506, // RFC 2295, 8.1
+ InsufficientStorage = 507, // RFC 4918, 11.5
+ LoopDetected = 508, // RFC 5842, 7.2
+ NotExtended = 510, // RFC 2774, 7
+ NetworkAuthenticationRequired = 511 // RFC 6585, 6
+}
+
+export const STATUS_TEXT = new Map<Status, string>([
+ [Status.Continue, "Continue"],
+ [Status.SwitchingProtocols, "Switching Protocols"],
+ [Status.Processing, "Processing"],
+
+ [Status.OK, "OK"],
+ [Status.Created, "Created"],
+ [Status.Accepted, "Accepted"],
+ [Status.NonAuthoritativeInfo, "Non-Authoritative Information"],
+ [Status.NoContent, "No Content"],
+ [Status.ResetContent, "Reset Content"],
+ [Status.PartialContent, "Partial Content"],
+ [Status.MultiStatus, "Multi-Status"],
+ [Status.AlreadyReported, "Already Reported"],
+ [Status.IMUsed, "IM Used"],
+
+ [Status.MultipleChoices, "Multiple Choices"],
+ [Status.MovedPermanently, "Moved Permanently"],
+ [Status.Found, "Found"],
+ [Status.SeeOther, "See Other"],
+ [Status.NotModified, "Not Modified"],
+ [Status.UseProxy, "Use Proxy"],
+ [Status.TemporaryRedirect, "Temporary Redirect"],
+ [Status.PermanentRedirect, "Permanent Redirect"],
+
+ [Status.BadRequest, "Bad Request"],
+ [Status.Unauthorized, "Unauthorized"],
+ [Status.PaymentRequired, "Payment Required"],
+ [Status.Forbidden, "Forbidden"],
+ [Status.NotFound, "Not Found"],
+ [Status.MethodNotAllowed, "Method Not Allowed"],
+ [Status.NotAcceptable, "Not Acceptable"],
+ [Status.ProxyAuthRequired, "Proxy Authentication Required"],
+ [Status.RequestTimeout, "Request Timeout"],
+ [Status.Conflict, "Conflict"],
+ [Status.Gone, "Gone"],
+ [Status.LengthRequired, "Length Required"],
+ [Status.PreconditionFailed, "Precondition Failed"],
+ [Status.RequestEntityTooLarge, "Request Entity Too Large"],
+ [Status.RequestURITooLong, "Request URI Too Long"],
+ [Status.UnsupportedMediaType, "Unsupported Media Type"],
+ [Status.RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable"],
+ [Status.ExpectationFailed, "Expectation Failed"],
+ [Status.Teapot, "I'm a teapot"],
+ [Status.MisdirectedRequest, "Misdirected Request"],
+ [Status.UnprocessableEntity, "Unprocessable Entity"],
+ [Status.Locked, "Locked"],
+ [Status.FailedDependency, "Failed Dependency"],
+ [Status.UpgradeRequired, "Upgrade Required"],
+ [Status.PreconditionRequired, "Precondition Required"],
+ [Status.TooManyRequests, "Too Many Requests"],
+ [Status.RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"],
+ [Status.UnavailableForLegalReasons, "Unavailable For Legal Reasons"],
+
+ [Status.InternalServerError, "Internal Server Error"],
+ [Status.NotImplemented, "Not Implemented"],
+ [Status.BadGateway, "Bad Gateway"],
+ [Status.ServiceUnavailable, "Service Unavailable"],
+ [Status.GatewayTimeout, "Gateway Timeout"],
+ [Status.HTTPVersionNotSupported, "HTTP Version Not Supported"],
+ [Status.VariantAlsoNegotiates, "Variant Also Negotiates"],
+ [Status.InsufficientStorage, "Insufficient Storage"],
+ [Status.LoopDetected, "Loop Detected"],
+ [Status.NotExtended, "Not Extended"],
+ [Status.NetworkAuthenticationRequired, "Network Authentication Required"]
+]);
diff --git a/http/http_test.ts b/http/http_test.ts
new file mode 100644
index 000000000..ba0cec3e3
--- /dev/null
+++ b/http/http_test.ts
@@ -0,0 +1,223 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Ported from
+// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go
+
+import { Buffer } from "deno";
+import { test, assert, assertEqual } from "../testing/mod.ts";
+import {
+ listenAndServe,
+ ServerRequest,
+ setContentLength,
+ Response
+} from "./mod.ts";
+import { BufWriter, BufReader } from "../io/bufio.ts";
+
+interface ResponseTest {
+ response: Response;
+ raw: string;
+}
+
+const enc = new TextEncoder();
+const dec = new TextDecoder();
+
+const responseTests: ResponseTest[] = [
+ // Default response
+ {
+ response: {},
+ raw: "HTTP/1.1 200 OK\r\n" + "\r\n"
+ },
+ // HTTP/1.1, chunked coding; empty trailer; close
+ {
+ response: {
+ status: 200,
+ body: new Buffer(new TextEncoder().encode("abcdef"))
+ },
+
+ raw:
+ "HTTP/1.1 200 OK\r\n" +
+ "transfer-encoding: chunked\r\n\r\n" +
+ "6\r\nabcdef\r\n0\r\n\r\n"
+ }
+];
+
+test(async function responseWrite() {
+ for (const testCase of responseTests) {
+ const buf = new Buffer();
+ const bufw = new BufWriter(buf);
+ const request = new ServerRequest();
+ request.w = bufw;
+
+ await request.respond(testCase.response);
+ assertEqual(buf.toString(), testCase.raw);
+ }
+});
+
+test(async function requestBodyWithContentLength() {
+ {
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("content-length", "5");
+ const buf = new Buffer(enc.encode("Hello"));
+ req.r = new BufReader(buf);
+ const body = dec.decode(await req.body());
+ assertEqual(body, "Hello");
+ }
+
+ // Larger than internal buf
+ {
+ const longText = "1234\n".repeat(1000);
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("Content-Length", "5000");
+ const buf = new Buffer(enc.encode(longText));
+ req.r = new BufReader(buf);
+ const body = dec.decode(await req.body());
+ assertEqual(body, longText);
+ }
+});
+
+test(async function requestBodyWithTransferEncoding() {
+ {
+ const shortText = "Hello";
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("transfer-encoding", "chunked");
+ let chunksData = "";
+ let chunkOffset = 0;
+ const maxChunkSize = 70;
+ while (chunkOffset < shortText.length) {
+ const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset);
+ chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr(
+ chunkOffset,
+ chunkSize
+ )}\r\n`;
+ chunkOffset += chunkSize;
+ }
+ chunksData += "0\r\n\r\n";
+ const buf = new Buffer(enc.encode(chunksData));
+ req.r = new BufReader(buf);
+ const body = dec.decode(await req.body());
+ assertEqual(body, shortText);
+ }
+
+ // Larger than internal buf
+ {
+ const longText = "1234\n".repeat(1000);
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("transfer-encoding", "chunked");
+ let chunksData = "";
+ let chunkOffset = 0;
+ const maxChunkSize = 70;
+ while (chunkOffset < longText.length) {
+ const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset);
+ chunksData += `${chunkSize.toString(16)}\r\n${longText.substr(
+ chunkOffset,
+ chunkSize
+ )}\r\n`;
+ chunkOffset += chunkSize;
+ }
+ chunksData += "0\r\n\r\n";
+ const buf = new Buffer(enc.encode(chunksData));
+ req.r = new BufReader(buf);
+ const body = dec.decode(await req.body());
+ assertEqual(body, longText);
+ }
+});
+
+test(async function requestBodyStreamWithContentLength() {
+ {
+ const shortText = "Hello";
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("content-length", "" + shortText.length);
+ const buf = new Buffer(enc.encode(shortText));
+ req.r = new BufReader(buf);
+ const it = await req.bodyStream();
+ let offset = 0;
+ for await (const chunk of it) {
+ const s = dec.decode(chunk);
+ assertEqual(shortText.substr(offset, s.length), s);
+ offset += s.length;
+ }
+ }
+
+ // Larger than internal buf
+ {
+ const longText = "1234\n".repeat(1000);
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("Content-Length", "5000");
+ const buf = new Buffer(enc.encode(longText));
+ req.r = new BufReader(buf);
+ const it = await req.bodyStream();
+ let offset = 0;
+ for await (const chunk of it) {
+ const s = dec.decode(chunk);
+ assertEqual(longText.substr(offset, s.length), s);
+ offset += s.length;
+ }
+ }
+});
+
+test(async function requestBodyStreamWithTransferEncoding() {
+ {
+ const shortText = "Hello";
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("transfer-encoding", "chunked");
+ let chunksData = "";
+ let chunkOffset = 0;
+ const maxChunkSize = 70;
+ while (chunkOffset < shortText.length) {
+ const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset);
+ chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr(
+ chunkOffset,
+ chunkSize
+ )}\r\n`;
+ chunkOffset += chunkSize;
+ }
+ chunksData += "0\r\n\r\n";
+ const buf = new Buffer(enc.encode(chunksData));
+ req.r = new BufReader(buf);
+ const it = await req.bodyStream();
+ let offset = 0;
+ for await (const chunk of it) {
+ const s = dec.decode(chunk);
+ assertEqual(shortText.substr(offset, s.length), s);
+ offset += s.length;
+ }
+ }
+
+ // Larger than internal buf
+ {
+ const longText = "1234\n".repeat(1000);
+ const req = new ServerRequest();
+ req.headers = new Headers();
+ req.headers.set("transfer-encoding", "chunked");
+ let chunksData = "";
+ let chunkOffset = 0;
+ const maxChunkSize = 70;
+ while (chunkOffset < longText.length) {
+ const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset);
+ chunksData += `${chunkSize.toString(16)}\r\n${longText.substr(
+ chunkOffset,
+ chunkSize
+ )}\r\n`;
+ chunkOffset += chunkSize;
+ }
+ chunksData += "0\r\n\r\n";
+ const buf = new Buffer(enc.encode(chunksData));
+ req.r = new BufReader(buf);
+ const it = await req.bodyStream();
+ let offset = 0;
+ for await (const chunk of it) {
+ const s = dec.decode(chunk);
+ assertEqual(longText.substr(offset, s.length), s);
+ offset += s.length;
+ }
+ }
+});
diff --git a/http/mod.ts b/http/mod.ts
new file mode 100644
index 000000000..217bd68b3
--- /dev/null
+++ b/http/mod.ts
@@ -0,0 +1,8 @@
+import {
+ serve,
+ listenAndServe,
+ Response,
+ setContentLength,
+ ServerRequest
+} from "./http.ts";
+export { serve, listenAndServe, Response, setContentLength, ServerRequest };