summaryrefslogtreecommitdiff
path: root/net
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2018-12-18 18:25:49 -0500
committerRyan Dahl <ry@tinyclouds.org>2018-12-18 18:27:05 -0500
commit968d50842512a007dc9c2e4ae31b1970fdc6e6a2 (patch)
tree240d8b019a6694b6722ea4cfc3576e3f5b1b4bd0 /net
parent6e077e71fa27dbb993a49cda211c5385a76ca6aa (diff)
Rename project to deno_std
Move typescript files to net/ Original: https://github.com/denoland/deno_std/commit/99e276eb89fbe0003bfa8d9e7b907ff3ef19ee47
Diffstat (limited to 'net')
-rw-r--r--net/bufio.ts465
-rw-r--r--net/bufio_test.ts345
-rwxr-xr-xnet/file_server.ts214
-rw-r--r--net/file_server_test.ts46
-rw-r--r--net/http.ts212
-rw-r--r--net/http_bench.ts15
-rw-r--r--net/http_status.ts134
-rw-r--r--net/http_test.ts58
-rw-r--r--net/iotest.ts61
-rw-r--r--net/textproto.ts149
-rw-r--r--net/textproto_test.ts93
-rw-r--r--net/util.ts29
12 files changed, 1821 insertions, 0 deletions
diff --git a/net/bufio.ts b/net/bufio.ts
new file mode 100644
index 000000000..b412cbce8
--- /dev/null
+++ b/net/bufio.ts
@@ -0,0 +1,465 @@
+// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio.go
+// Copyright 2009 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.
+
+import { Reader, ReadResult, Writer } from "deno";
+import { assert, charCode, copyBytes } from "./util.ts";
+
+const DEFAULT_BUF_SIZE = 4096;
+const MIN_BUF_SIZE = 16;
+const MAX_CONSECUTIVE_EMPTY_READS = 100;
+const CR = charCode("\r");
+const LF = charCode("\n");
+
+export type BufState =
+ | null
+ | "EOF"
+ | "BufferFull"
+ | "ShortWrite"
+ | "NoProgress"
+ | Error;
+
+/** BufReader implements buffering for a Reader object. */
+export class BufReader implements Reader {
+ private buf: Uint8Array;
+ private rd: Reader; // Reader provided by caller.
+ private r = 0; // buf read position.
+ private w = 0; // buf write position.
+ private lastByte: number;
+ private lastCharSize: number;
+ private err: BufState;
+
+ constructor(rd: Reader, size = DEFAULT_BUF_SIZE) {
+ if (size < MIN_BUF_SIZE) {
+ size = MIN_BUF_SIZE;
+ }
+ this._reset(new Uint8Array(size), rd);
+ }
+
+ /** Returns the size of the underlying buffer in bytes. */
+ size(): number {
+ return this.buf.byteLength;
+ }
+
+ buffered(): number {
+ return this.w - this.r;
+ }
+
+ private _readErr(): BufState {
+ const err = this.err;
+ this.err = null;
+ return err;
+ }
+
+ // Reads a new chunk into the buffer.
+ private async _fill(): Promise<void> {
+ // Slide existing data to beginning.
+ if (this.r > 0) {
+ this.buf.copyWithin(0, this.r, this.w);
+ this.w -= this.r;
+ this.r = 0;
+ }
+
+ if (this.w >= this.buf.byteLength) {
+ throw Error("bufio: tried to fill full buffer");
+ }
+
+ // Read new data: try a limited number of times.
+ for (let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--) {
+ let rr: ReadResult;
+ try {
+ rr = await this.rd.read(this.buf.subarray(this.w));
+ } catch (e) {
+ this.err = e;
+ return;
+ }
+ assert(rr.nread >= 0, "negative read");
+ this.w += rr.nread;
+ if (rr.eof) {
+ this.err = "EOF";
+ return;
+ }
+ if (rr.nread > 0) {
+ return;
+ }
+ }
+ this.err = "NoProgress";
+ }
+
+ /** Discards any buffered data, resets all state, and switches
+ * the buffered reader to read from r.
+ */
+ reset(r: Reader): void {
+ this._reset(this.buf, r);
+ }
+
+ private _reset(buf: Uint8Array, rd: Reader): void {
+ this.buf = buf;
+ this.rd = rd;
+ this.lastByte = -1;
+ // this.lastRuneSize = -1;
+ }
+
+ /** reads data into p.
+ * It returns the number of bytes read into p.
+ * The bytes are taken from at most one Read on the underlying Reader,
+ * hence n may be less than len(p).
+ * At EOF, the count will be zero and err will be io.EOF.
+ * To read exactly len(p) bytes, use io.ReadFull(b, p).
+ */
+ async read(p: Uint8Array): Promise<ReadResult> {
+ let rr: ReadResult = { nread: p.byteLength, eof: false };
+ if (rr.nread === 0) {
+ if (this.err) {
+ throw this._readErr();
+ }
+ return rr;
+ }
+
+ if (this.r === this.w) {
+ if (this.err) {
+ throw this._readErr();
+ }
+ if (p.byteLength >= this.buf.byteLength) {
+ // Large read, empty buffer.
+ // Read directly into p to avoid copy.
+ rr = await this.rd.read(p);
+ assert(rr.nread >= 0, "negative read");
+ if (rr.nread > 0) {
+ this.lastByte = p[rr.nread - 1];
+ // this.lastRuneSize = -1;
+ }
+ if (this.err) {
+ throw this._readErr();
+ }
+ return rr;
+ }
+ // One read.
+ // Do not use this.fill, which will loop.
+ this.r = 0;
+ this.w = 0;
+ try {
+ rr = await this.rd.read(this.buf);
+ } catch (e) {
+ this.err = e;
+ }
+ assert(rr.nread >= 0, "negative read");
+ if (rr.nread === 0) {
+ if (this.err) {
+ throw this._readErr();
+ }
+ return rr;
+ }
+ this.w += rr.nread;
+ }
+
+ // copy as much as we can
+ rr.nread = copyBytes(p as Uint8Array, this.buf.subarray(this.r, this.w), 0);
+ this.r += rr.nread;
+ this.lastByte = this.buf[this.r - 1];
+ // this.lastRuneSize = -1;
+ return rr;
+ }
+
+ /** reads exactly len(p) bytes into p.
+ * Ported from https://golang.org/pkg/io/#ReadFull
+ * It returns the number of bytes copied and an error if fewer bytes were read.
+ * The error is EOF only if no bytes were read.
+ * If an EOF happens after reading some but not all the bytes,
+ * readFull returns ErrUnexpectedEOF. ("EOF" for current impl)
+ * On return, n == len(p) if and only if err == nil.
+ * If r returns an error having read at least len(buf) bytes,
+ * the error is dropped.
+ */
+ async readFull(p: Uint8Array): Promise<[number, BufState]> {
+ let rr = await this.read(p);
+ let nread = rr.nread;
+ if (rr.eof) {
+ return [nread, nread < p.length ? "EOF" : null];
+ }
+ while (!rr.eof && nread < p.length) {
+ rr = await this.read(p.subarray(nread));
+ nread += rr.nread;
+ }
+ return [nread, nread < p.length ? "EOF" : null];
+ }
+
+
+ /** Returns the next byte [0, 255] or -1 if EOF. */
+ async readByte(): Promise<number> {
+ while (this.r === this.w) {
+ await this._fill(); // buffer is empty.
+ if (this.err == "EOF") {
+ return -1;
+ }
+ if (this.err != null) {
+ throw this._readErr();
+ }
+ }
+ const c = this.buf[this.r];
+ this.r++;
+ this.lastByte = c;
+ return c;
+ }
+
+ /** readString() reads until the first occurrence of delim in the input,
+ * returning a string containing the data up to and including the delimiter.
+ * If ReadString encounters an error before finding a delimiter,
+ * it returns the data read before the error and the error itself (often io.EOF).
+ * ReadString returns err != nil if and only if the returned data does not end in
+ * delim.
+ * For simple uses, a Scanner may be more convenient.
+ */
+ async readString(delim: string): Promise<string> {
+ throw new Error("Not implemented");
+ }
+
+ /** readLine() is a low-level line-reading primitive. Most callers should use
+ * readBytes('\n') or readString('\n') instead or use a Scanner.
+ *
+ * readLine tries to return a single line, not including the end-of-line bytes.
+ * If the line was too long for the buffer then isPrefix is set and the
+ * beginning of the line is returned. The rest of the line will be returned
+ * from future calls. isPrefix will be false when returning the last fragment
+ * of the line. The returned buffer is only valid until the next call to
+ * ReadLine. ReadLine either returns a non-nil line or it returns an error,
+ * never both.
+ *
+ * The text returned from ReadLine does not include the line end ("\r\n" or "\n").
+ * No indication or error is given if the input ends without a final line end.
+ * Calling UnreadByte after ReadLine will always unread the last byte read
+ * (possibly a character belonging to the line end) even if that byte is not
+ * part of the line returned by ReadLine.
+ */
+ async readLine(): Promise<[Uint8Array, boolean, BufState]> {
+ let [line, err] = await this.readSlice(LF);
+
+ if (err === "BufferFull") {
+ // Handle the case where "\r\n" straddles the buffer.
+ if (line.byteLength > 0 && line[line.byteLength - 1] === CR) {
+ // Put the '\r' back on buf and drop it from line.
+ // Let the next call to ReadLine check for "\r\n".
+ assert(this.r > 0, "bufio: tried to rewind past start of buffer");
+ this.r--;
+ line = line.subarray(0, line.byteLength - 1);
+ }
+ return [line, true, null];
+ }
+
+ if (line.byteLength === 0) {
+ return [line, false, err];
+ }
+ err = null;
+
+ if (line[line.byteLength - 1] == LF) {
+ let drop = 1;
+ if (line.byteLength > 1 && line[line.byteLength - 2] === CR) {
+ drop = 2;
+ }
+ line = line.subarray(0, line.byteLength - drop);
+ }
+ return [line, false, err];
+ }
+
+ /** readSlice() reads until the first occurrence of delim in the input,
+ * returning a slice pointing at the bytes in the buffer. The bytes stop
+ * being valid at the next read. If readSlice() encounters an error before
+ * finding a delimiter, it returns all the data in the buffer and the error
+ * itself (often io.EOF). readSlice() fails with error ErrBufferFull if the
+ * buffer fills without a delim. Because the data returned from readSlice()
+ * will be overwritten by the next I/O operation, most clients should use
+ * readBytes() or readString() instead. readSlice() returns err != nil if and
+ * only if line does not end in delim.
+ */
+ async readSlice(delim: number): Promise<[Uint8Array, BufState]> {
+ let s = 0; // search start index
+ let line: Uint8Array;
+ let err: BufState;
+ while (true) {
+ // Search buffer.
+ let i = this.buf.subarray(this.r + s, this.w).indexOf(delim);
+ if (i >= 0) {
+ i += s;
+ line = this.buf.subarray(this.r, this.r + i + 1);
+ this.r += i + 1;
+ break;
+ }
+
+ // Pending error?
+ if (this.err) {
+ line = this.buf.subarray(this.r, this.w);
+ this.r = this.w;
+ err = this._readErr();
+ break;
+ }
+
+ // Buffer full?
+ if (this.buffered() >= this.buf.byteLength) {
+ this.r = this.w;
+ line = this.buf;
+ err = "BufferFull";
+ break;
+ }
+
+ s = this.w - this.r; // do not rescan area we scanned before
+
+ await this._fill(); // buffer is not full
+ }
+
+ // Handle last byte, if any.
+ let i = line.byteLength - 1;
+ if (i >= 0) {
+ this.lastByte = line[i];
+ // this.lastRuneSize = -1
+ }
+
+ return [line, err];
+ }
+
+ /** Peek returns the next n bytes without advancing the reader. The bytes stop
+ * being valid at the next read call. If Peek returns fewer than n bytes, it
+ * also returns an error explaining why the read is short. The error is
+ * ErrBufferFull if n is larger than b's buffer size.
+ */
+ async peek(n: number): Promise<[Uint8Array, BufState]> {
+ if (n < 0) {
+ throw Error("negative count");
+ }
+
+ while (
+ this.w - this.r < n &&
+ this.w - this.r < this.buf.byteLength &&
+ this.err == null
+ ) {
+ await this._fill(); // this.w - this.r < len(this.buf) => buffer is not full
+ }
+
+ if (n > this.buf.byteLength) {
+ return [this.buf.subarray(this.r, this.w), "BufferFull"];
+ }
+
+ // 0 <= n <= len(this.buf)
+ let err: BufState;
+ let avail = this.w - this.r;
+ if (avail < n) {
+ // not enough data in buffer
+ n = avail;
+ err = this._readErr();
+ if (!err) {
+ err = "BufferFull";
+ }
+ }
+ return [this.buf.subarray(this.r, this.r + n), err];
+ }
+}
+
+/** BufWriter implements buffering for an deno.Writer object.
+ * If an error occurs writing to a Writer, no more data will be
+ * accepted and all subsequent writes, and flush(), will return the error.
+ * After all data has been written, the client should call the
+ * flush() method to guarantee all data has been forwarded to
+ * the underlying deno.Writer.
+ */
+export class BufWriter implements Writer {
+ buf: Uint8Array;
+ n: number = 0;
+ err: null | BufState = null;
+
+ constructor(private wr: Writer, size = DEFAULT_BUF_SIZE) {
+ if (size <= 0) {
+ size = DEFAULT_BUF_SIZE;
+ }
+ this.buf = new Uint8Array(size);
+ }
+
+ /** Size returns the size of the underlying buffer in bytes. */
+ size(): number {
+ return this.buf.byteLength;
+ }
+
+ /** Discards any unflushed buffered data, clears any error, and
+ * resets b to write its output to w.
+ */
+ reset(w: Writer): void {
+ this.err = null;
+ this.n = 0;
+ this.wr = w;
+ }
+
+ /** Flush writes any buffered data to the underlying io.Writer. */
+ async flush(): Promise<BufState> {
+ if (this.err != null) {
+ return this.err;
+ }
+ if (this.n == 0) {
+ return null;
+ }
+
+ let n: number;
+ let err: BufState = null;
+ try {
+ n = await this.wr.write(this.buf.subarray(0, this.n));
+ } catch (e) {
+ err = e;
+ }
+
+ if (n < this.n && err == null) {
+ err = "ShortWrite";
+ }
+
+ if (err != null) {
+ if (n > 0 && n < this.n) {
+ this.buf.copyWithin(0, n, this.n);
+ }
+ this.n -= n;
+ this.err = err;
+ return err;
+ }
+ this.n = 0;
+ }
+
+ /** Returns how many bytes are unused in the buffer. */
+ available(): number {
+ return this.buf.byteLength - this.n;
+ }
+
+ /** buffered returns the number of bytes that have been written into the
+ * current buffer.
+ */
+ buffered(): number {
+ return this.n;
+ }
+
+ /** Writes the contents of p into the buffer.
+ * Returns the number of bytes written.
+ */
+ async write(p: Uint8Array): Promise<number> {
+ let nn = 0;
+ let n: number;
+ while (p.byteLength > this.available() && !this.err) {
+ if (this.buffered() == 0) {
+ // Large write, empty buffer.
+ // Write directly from p to avoid copy.
+ try {
+ n = await this.wr.write(p);
+ } catch (e) {
+ this.err = e;
+ }
+ } else {
+ n = copyBytes(this.buf, p, this.n);
+ this.n += n;
+ await this.flush();
+ }
+ nn += n;
+ p = p.subarray(n);
+ }
+ if (this.err) {
+ throw this.err;
+ }
+ n = copyBytes(this.buf, p, this.n);
+ this.n += n;
+ nn += n;
+ return nn;
+ }
+}
diff --git a/net/bufio_test.ts b/net/bufio_test.ts
new file mode 100644
index 000000000..19954bdf6
--- /dev/null
+++ b/net/bufio_test.ts
@@ -0,0 +1,345 @@
+// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio_test.go
+// Copyright 2009 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.
+
+import { Buffer, Reader, ReadResult } from "deno";
+import {
+ test,
+ assert,
+ assertEqual
+} from "https://deno.land/x/testing/testing.ts";
+import { BufReader, BufState, BufWriter } from "./bufio.ts";
+import * as iotest from "./iotest.ts";
+import { charCode, copyBytes, stringsReader } from "./util.ts";
+
+const encoder = new TextEncoder();
+
+async function readBytes(buf: BufReader): Promise<string> {
+ const b = new Uint8Array(1000);
+ let nb = 0;
+ while (true) {
+ let c = await buf.readByte();
+ if (c < 0) {
+ break; // EOF
+ }
+ b[nb] = c;
+ nb++;
+ }
+ const decoder = new TextDecoder();
+ return decoder.decode(b.subarray(0, nb));
+}
+
+test(async function bufioReaderSimple() {
+ const data = "hello world";
+ const b = new BufReader(stringsReader(data));
+ const s = await readBytes(b);
+ assertEqual(s, data);
+});
+
+type ReadMaker = { name: string; fn: (r: Reader) => Reader };
+
+const readMakers: ReadMaker[] = [
+ { name: "full", fn: r => r },
+ { name: "byte", fn: r => new iotest.OneByteReader(r) },
+ { name: "half", fn: r => new iotest.HalfReader(r) }
+ // TODO { name: "data+err", r => new iotest.DataErrReader(r) },
+ // { name: "timeout", fn: r => new iotest.TimeoutReader(r) },
+];
+
+function readLines(b: BufReader): string {
+ let s = "";
+ while (true) {
+ let s1 = b.readString("\n");
+ if (s1 == null) {
+ break; // EOF
+ }
+ s += s1;
+ }
+ return s;
+}
+
+// Call read to accumulate the text of a file
+async function reads(buf: BufReader, m: number): Promise<string> {
+ const b = new Uint8Array(1000);
+ let nb = 0;
+ while (true) {
+ const { nread, eof } = await buf.read(b.subarray(nb, nb + m));
+ nb += nread;
+ if (eof) {
+ break;
+ }
+ }
+ const decoder = new TextDecoder();
+ return decoder.decode(b.subarray(0, nb));
+}
+
+type NamedBufReader = { name: string; fn: (r: BufReader) => Promise<string> };
+
+const bufreaders: NamedBufReader[] = [
+ { name: "1", fn: (b: BufReader) => reads(b, 1) },
+ { name: "2", fn: (b: BufReader) => reads(b, 2) },
+ { name: "3", fn: (b: BufReader) => reads(b, 3) },
+ { name: "4", fn: (b: BufReader) => reads(b, 4) },
+ { name: "5", fn: (b: BufReader) => reads(b, 5) },
+ { name: "7", fn: (b: BufReader) => reads(b, 7) },
+ { name: "bytes", fn: readBytes }
+ // { name: "lines", fn: readLines },
+];
+
+const MIN_READ_BUFFER_SIZE = 16;
+const bufsizes: number[] = [
+ 0,
+ MIN_READ_BUFFER_SIZE,
+ 23,
+ 32,
+ 46,
+ 64,
+ 93,
+ 128,
+ 1024,
+ 4096
+];
+
+test(async function bufioBufReader() {
+ const texts = new Array<string>(31);
+ let str = "";
+ let all = "";
+ for (let i = 0; i < texts.length - 1; i++) {
+ texts[i] = str + "\n";
+ all += texts[i];
+ str += String.fromCharCode(i % 26 + 97);
+ }
+ texts[texts.length - 1] = all;
+
+ for (let text of texts) {
+ for (let readmaker of readMakers) {
+ for (let bufreader of bufreaders) {
+ for (let bufsize of bufsizes) {
+ const read = readmaker.fn(stringsReader(text));
+ const buf = new BufReader(read, bufsize);
+ const s = await bufreader.fn(buf);
+ const debugStr =
+ `reader=${readmaker.name} ` +
+ `fn=${bufreader.name} bufsize=${bufsize} want=${text} got=${s}`;
+ assertEqual(s, text, debugStr);
+ }
+ }
+ }
+ }
+});
+
+test(async function bufioBufferFull() {
+ const longString =
+ "And now, hello, world! It is the time for all good men to come to the aid of their party";
+ const buf = new BufReader(stringsReader(longString), MIN_READ_BUFFER_SIZE);
+ let [line, err] = await buf.readSlice(charCode("!"));
+
+ const decoder = new TextDecoder();
+ let actual = decoder.decode(line);
+ assertEqual(err, "BufferFull");
+ assertEqual(actual, "And now, hello, ");
+
+ [line, err] = await buf.readSlice(charCode("!"));
+ actual = decoder.decode(line);
+ assertEqual(actual, "world!");
+ assert(err == null);
+});
+
+const testInput = encoder.encode(
+ "012\n345\n678\n9ab\ncde\nfgh\nijk\nlmn\nopq\nrst\nuvw\nxy"
+);
+const testInputrn = encoder.encode(
+ "012\r\n345\r\n678\r\n9ab\r\ncde\r\nfgh\r\nijk\r\nlmn\r\nopq\r\nrst\r\nuvw\r\nxy\r\n\n\r\n"
+);
+const testOutput = encoder.encode("0123456789abcdefghijklmnopqrstuvwxy");
+
+// TestReader wraps a Uint8Array and returns reads of a specific length.
+class TestReader implements Reader {
+ constructor(private data: Uint8Array, private stride: number) {}
+
+ async read(buf: Uint8Array): Promise<ReadResult> {
+ let nread = this.stride;
+ if (nread > this.data.byteLength) {
+ nread = this.data.byteLength;
+ }
+ if (nread > buf.byteLength) {
+ nread = buf.byteLength;
+ }
+ copyBytes(buf as Uint8Array, this.data);
+ this.data = this.data.subarray(nread);
+ let eof = false;
+ if (this.data.byteLength == 0) {
+ eof = true;
+ }
+ return { nread, eof };
+ }
+}
+
+async function testReadLine(input: Uint8Array): Promise<void> {
+ for (let stride = 1; stride < 2; stride++) {
+ let done = 0;
+ let reader = new TestReader(input, stride);
+ let l = new BufReader(reader, input.byteLength + 1);
+ while (true) {
+ let [line, isPrefix, err] = await l.readLine();
+ if (line.byteLength > 0 && err != null) {
+ throw Error("readLine returned both data and error");
+ }
+ assertEqual(isPrefix, false);
+ if (err == "EOF") {
+ break;
+ }
+ let want = testOutput.subarray(done, done + line.byteLength);
+ assertEqual(
+ line,
+ want,
+ `Bad line at stride ${stride}: want: ${want} got: ${line}`
+ );
+ done += line.byteLength;
+ }
+ assertEqual(
+ done,
+ testOutput.byteLength,
+ `readLine didn't return everything: got: ${done}, ` +
+ `want: ${testOutput} (stride: ${stride})`
+ );
+ }
+}
+
+test(async function bufioReadLine() {
+ await testReadLine(testInput);
+ await testReadLine(testInputrn);
+});
+
+test(async function bufioPeek() {
+ const decoder = new TextDecoder();
+ let p = new Uint8Array(10);
+ // string is 16 (minReadBufferSize) long.
+ let buf = new BufReader(
+ stringsReader("abcdefghijklmnop"),
+ MIN_READ_BUFFER_SIZE
+ );
+
+ let [actual, err] = await buf.peek(1);
+ assertEqual(decoder.decode(actual), "a");
+ assert(err == null);
+
+ [actual, err] = await buf.peek(4);
+ assertEqual(decoder.decode(actual), "abcd");
+ assert(err == null);
+
+ [actual, err] = await buf.peek(32);
+ assertEqual(decoder.decode(actual), "abcdefghijklmnop");
+ assertEqual(err, "BufferFull");
+
+ await buf.read(p.subarray(0, 3));
+ assertEqual(decoder.decode(p.subarray(0, 3)), "abc");
+
+ [actual, err] = await buf.peek(1);
+ assertEqual(decoder.decode(actual), "d");
+ assert(err == null);
+
+ [actual, err] = await buf.peek(1);
+ assertEqual(decoder.decode(actual), "d");
+ assert(err == null);
+
+ [actual, err] = await buf.peek(1);
+ assertEqual(decoder.decode(actual), "d");
+ assert(err == null);
+
+ [actual, err] = await buf.peek(2);
+ assertEqual(decoder.decode(actual), "de");
+ assert(err == null);
+
+ let { eof } = await buf.read(p.subarray(0, 3));
+ assertEqual(decoder.decode(p.subarray(0, 3)), "def");
+ assert(!eof);
+ assert(err == null);
+
+ [actual, err] = await buf.peek(4);
+ assertEqual(decoder.decode(actual), "ghij");
+ assert(err == null);
+
+ await buf.read(p);
+ assertEqual(decoder.decode(p), "ghijklmnop");
+
+ [actual, err] = await buf.peek(0);
+ assertEqual(decoder.decode(actual), "");
+ assert(err == null);
+
+ [actual, err] = await buf.peek(1);
+ assertEqual(decoder.decode(actual), "");
+ assert(err == "EOF");
+ /* TODO
+ // Test for issue 3022, not exposing a reader's error on a successful Peek.
+ buf = NewReaderSize(dataAndEOFReader("abcd"), 32)
+ if s, err := buf.Peek(2); string(s) != "ab" || err != nil {
+ t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err)
+ }
+ if s, err := buf.Peek(4); string(s) != "abcd" || err != nil {
+ t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err)
+ }
+ if n, err := buf.Read(p[0:5]); string(p[0:n]) != "abcd" || err != nil {
+ t.Fatalf("Read after peek = %q, %v; want abcd, EOF", p[0:n], err)
+ }
+ if n, err := buf.Read(p[0:1]); string(p[0:n]) != "" || err != io.EOF {
+ t.Fatalf(`second Read after peek = %q, %v; want "", EOF`, p[0:n], err)
+ }
+ */
+});
+
+test(async function bufioWriter() {
+ const data = new Uint8Array(8192);
+
+ for (let i = 0; i < data.byteLength; i++) {
+ data[i] = charCode(" ") + i % (charCode("~") - charCode(" "));
+ }
+
+ const w = new Buffer();
+ for (let nwrite of bufsizes) {
+ for (let bs of bufsizes) {
+ // Write nwrite bytes using buffer size bs.
+ // Check that the right amount makes it out
+ // and that the data is correct.
+
+ w.reset();
+ const buf = new BufWriter(w, bs);
+
+ const context = `nwrite=${nwrite} bufsize=${bs}`;
+ const n = await buf.write(data.subarray(0, nwrite));
+ assertEqual(n, nwrite, context);
+
+ await buf.flush();
+
+ const written = w.bytes();
+ assertEqual(written.byteLength, nwrite);
+
+ for (let l = 0; l < written.byteLength; l++) {
+ assertEqual(written[l], data[l]);
+ }
+ }
+ }
+});
+
+test(async function bufReaderReadFull() {
+ const enc = new TextEncoder();
+ const dec = new TextDecoder();
+ const text = "Hello World";
+ const data = new Buffer(enc.encode(text));
+ const bufr = new BufReader(data, 3);
+ {
+ const buf = new Uint8Array(6);
+ const [nread, err] = await bufr.readFull(buf);
+ assertEqual(nread, 6);
+ assert(!err);
+ assertEqual(dec.decode(buf), "Hello ");
+ }
+ {
+ const buf = new Uint8Array(6);
+ const [nread, err] = await bufr.readFull(buf);
+ assertEqual(nread, 5);
+ assertEqual(err, "EOF");
+ assertEqual(dec.decode(buf.subarray(0, 5)), "World");
+ }
+});
diff --git a/net/file_server.ts b/net/file_server.ts
new file mode 100755
index 000000000..bd1c52b88
--- /dev/null
+++ b/net/file_server.ts
@@ -0,0 +1,214 @@
+#!/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 "./http";
+import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno";
+
+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>
+`;
+
+let currentDir = cwd();
+const target = args[1];
+if (target) {
+ currentDir = `${currentDir}/${target}`;
+}
+const addr = `0.0.0.0:${args[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());
+
+ 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);
+}
+
+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 {
+ serverLog(req, response);
+ req.respond(response);
+ }
+});
+
+console.log(`HTTP server listening on http://${addr}/`);
diff --git a/net/file_server_test.ts b/net/file_server_test.ts
new file mode 100644
index 000000000..a04ced7e5
--- /dev/null
+++ b/net/file_server_test.ts
@@ -0,0 +1,46 @@
+import { readFile } from "deno";
+
+import {
+ test,
+ assert,
+ assertEqual
+} from "https://deno.land/x/testing/testing.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/.travis.yml");
+ const downloadedFile = await res.text();
+ const localFile = new TextDecoder().decode(await readFile("./.travis.yml"));
+ assertEqual(downloadedFile, localFile);
+ maybeCompleteTests();
+ });
+
+ test(async function serveDirectory() {
+ await serverReadyPromise;
+ const res = await fetch("http://localhost:4500/");
+ const page = await res.text();
+ assert(page.includes(".travis.yml"));
+ maybeCompleteTests();
+ });
+
+ test(async function serveFallback() {
+ await serverReadyPromise;
+ const res = await fetch("http://localhost:4500/badfile.txt");
+ assertEqual(res.status, 404);
+ maybeCompleteTests();
+ });
+}
diff --git a/net/http.ts b/net/http.ts
new file mode 100644
index 000000000..bd45aea0d
--- /dev/null
+++ b/net/http.ts
@@ -0,0 +1,212 @@
+import { listen, Conn, toAsyncIterator, Reader, copy } from "deno";
+import { BufReader, BufState, BufWriter } from "./bufio.ts";
+import { TextProtoReader } from "./textproto.ts";
+import { STATUS_TEXT } from "./http_status";
+import { assert } from "./util";
+
+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
+// Mutually calling with maybeHandleReq
+// TODO: make them async function after this change is done
+// https://github.com/tc39/ecma262/pull/1250
+// See https://v8.dev/blog/fast-async
+export function serveConn(env: ServeEnv, conn: Conn) {
+ readRequest(conn).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
+ // TODO: protection against client req flooding
+ serveConn(env, conn); // try read more (reusing connection)
+}
+
+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;
+ }
+ }
+ 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;
+ w: BufWriter;
+
+ 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): Promise<[ServerRequest, BufState]> {
+ const bufr = new BufReader(c);
+ const bufw = new BufWriter(c);
+ const req = new ServerRequest();
+ 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();
+
+ // TODO: handle body
+
+ return [req, err];
+}
diff --git a/net/http_bench.ts b/net/http_bench.ts
new file mode 100644
index 000000000..8e1e24ad6
--- /dev/null
+++ b/net/http_bench.ts
@@ -0,0 +1,15 @@
+import * as deno from "deno";
+import { serve } from "./http.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/net/http_status.ts b/net/http_status.ts
new file mode 100644
index 000000000..a3006d319
--- /dev/null
+++ b/net/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/net/http_test.ts b/net/http_test.ts
new file mode 100644
index 000000000..cdb7f8303
--- /dev/null
+++ b/net/http_test.ts
@@ -0,0 +1,58 @@
+// 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 {
+ test,
+ assert,
+ assertEqual
+} from "https://deno.land/x/testing/testing.ts";
+
+import {
+ listenAndServe,
+ ServerRequest,
+ setContentLength,
+ Response
+} from "./http";
+import { Buffer } from "deno";
+import { BufWriter } from "./bufio";
+
+interface ResponseTest {
+ response: Response;
+ raw: string;
+}
+
+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);
+ }
+});
diff --git a/net/iotest.ts b/net/iotest.ts
new file mode 100644
index 000000000..e3a42f58a
--- /dev/null
+++ b/net/iotest.ts
@@ -0,0 +1,61 @@
+// Ported to Deno from
+// Copyright 2009 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.
+
+import { Reader, ReadResult } from "deno";
+
+/** OneByteReader returns a Reader that implements
+ * each non-empty Read by reading one byte from r.
+ */
+export class OneByteReader implements Reader {
+ constructor(readonly r: Reader) {}
+
+ async read(p: Uint8Array): Promise<ReadResult> {
+ if (p.byteLength === 0) {
+ return { nread: 0, eof: false };
+ }
+ if (!(p instanceof Uint8Array)) {
+ throw Error("expected Uint8Array");
+ }
+ return this.r.read(p.subarray(0, 1));
+ }
+}
+
+/** HalfReader returns a Reader that implements Read
+ * by reading half as many requested bytes from r.
+ */
+export class HalfReader implements Reader {
+ constructor(readonly r: Reader) {}
+
+ async read(p: Uint8Array): Promise<ReadResult> {
+ if (!(p instanceof Uint8Array)) {
+ throw Error("expected Uint8Array");
+ }
+ const half = Math.floor((p.byteLength + 1) / 2);
+ return this.r.read(p.subarray(0, half));
+ }
+}
+
+export class ErrTimeout extends Error {
+ constructor() {
+ super("timeout");
+ this.name = "ErrTimeout";
+ }
+}
+
+/** TimeoutReader returns ErrTimeout on the second read
+ * with no data. Subsequent calls to read succeed.
+ */
+export class TimeoutReader implements Reader {
+ count = 0;
+ constructor(readonly r: Reader) {}
+
+ async read(p: Uint8Array): Promise<ReadResult> {
+ this.count++;
+ if (this.count === 2) {
+ throw new ErrTimeout();
+ }
+ return this.r.read(p);
+ }
+}
diff --git a/net/textproto.ts b/net/textproto.ts
new file mode 100644
index 000000000..342d74b33
--- /dev/null
+++ b/net/textproto.ts
@@ -0,0 +1,149 @@
+// Based on https://github.com/golang/go/blob/891682/src/net/textproto/
+// Copyright 2009 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.
+
+import { BufReader, BufState } from "./bufio.ts";
+import { charCode } from "./util.ts";
+
+const asciiDecoder = new TextDecoder();
+function str(buf: Uint8Array): string {
+ if (buf == null) {
+ return "";
+ } else {
+ return asciiDecoder.decode(buf);
+ }
+}
+
+export class ProtocolError extends Error {
+ constructor(msg: string) {
+ super(msg);
+ this.name = "ProtocolError";
+ }
+}
+
+export class TextProtoReader {
+ constructor(readonly r: BufReader) {}
+
+ /** readLine() reads a single line from the TextProtoReader,
+ * eliding the final \n or \r\n from the returned string.
+ */
+ async readLine(): Promise<[string, BufState]> {
+ let [line, err] = await this.readLineSlice();
+ return [str(line), err];
+ }
+
+ /** ReadMIMEHeader reads a MIME-style header from r.
+ * The header is a sequence of possibly continued Key: Value lines
+ * ending in a blank line.
+ * The returned map m maps CanonicalMIMEHeaderKey(key) to a
+ * sequence of values in the same order encountered in the input.
+ *
+ * For example, consider this input:
+ *
+ * My-Key: Value 1
+ * Long-Key: Even
+ * Longer Value
+ * My-Key: Value 2
+ *
+ * Given that input, ReadMIMEHeader returns the map:
+ *
+ * map[string][]string{
+ * "My-Key": {"Value 1", "Value 2"},
+ * "Long-Key": {"Even Longer Value"},
+ * }
+ */
+ async readMIMEHeader(): Promise<[Headers, BufState]> {
+ let m = new Headers();
+ let line: Uint8Array;
+
+ // The first line cannot start with a leading space.
+ let [buf, err] = await this.r.peek(1);
+ if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) {
+ [line, err] = await this.readLineSlice();
+ }
+
+ [buf, err] = await this.r.peek(1);
+ if (err == null && (buf[0] == charCode(" ") || buf[0] == charCode("\t"))) {
+ throw new ProtocolError(
+ `malformed MIME header initial line: ${str(line)}`
+ );
+ }
+
+ while (true) {
+ let [kv, err] = await this.readLineSlice(); // readContinuedLineSlice
+ if (kv.byteLength == 0) {
+ return [m, err];
+ }
+
+ // Key ends at first colon; should not have trailing spaces
+ // but they appear in the wild, violating specs, so we remove
+ // them if present.
+ let i = kv.indexOf(charCode(":"));
+ if (i < 0) {
+ throw new ProtocolError(`malformed MIME header line: ${str(kv)}`);
+ }
+ let endKey = i;
+ while (endKey > 0 && kv[endKey - 1] == charCode(" ")) {
+ endKey--;
+ }
+
+ //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey));
+ let key = str(kv.subarray(0, endKey));
+
+ // As per RFC 7230 field-name is a token, tokens consist of one or more chars.
+ // We could return a ProtocolError here, but better to be liberal in what we
+ // accept, so if we get an empty key, skip it.
+ if (key == "") {
+ continue;
+ }
+
+ // Skip initial spaces in value.
+ i++; // skip colon
+ while (
+ i < kv.byteLength &&
+ (kv[i] == charCode(" ") || kv[i] == charCode("\t"))
+ ) {
+ i++;
+ }
+ let value = str(kv.subarray(i));
+
+ m.append(key, value);
+
+ if (err != null) {
+ throw err;
+ }
+ }
+ }
+
+ async readLineSlice(): Promise<[Uint8Array, BufState]> {
+ // this.closeDot();
+ let line: null | Uint8Array;
+ while (true) {
+ let [l, more, err] = await this.r.readLine();
+ if (err != null) {
+ return [null, err];
+ }
+ // Avoid the copy if the first call produced a full line.
+ if (line == null && !more) {
+ return [l, null];
+ }
+ line = append(line, l);
+ if (!more) {
+ break;
+ }
+ }
+ return [line, null];
+ }
+}
+
+export function append(a: Uint8Array, b: Uint8Array): Uint8Array {
+ if (a == null) {
+ return b;
+ } else {
+ const output = new Uint8Array(a.length + b.length);
+ output.set(a, 0);
+ output.set(b, a.length);
+ return output;
+ }
+}
diff --git a/net/textproto_test.ts b/net/textproto_test.ts
new file mode 100644
index 000000000..25c12b0e8
--- /dev/null
+++ b/net/textproto_test.ts
@@ -0,0 +1,93 @@
+// Based on https://github.com/golang/go/blob/891682/src/net/textproto/
+// Copyright 2009 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.
+
+import { BufReader } from "./bufio.ts";
+import { TextProtoReader, append } from "./textproto.ts";
+import { stringsReader } from "./util.ts";
+import {
+ test,
+ assert,
+ assertEqual
+} from "https://deno.land/x/testing/testing.ts";
+
+function reader(s: string): TextProtoReader {
+ return new TextProtoReader(new BufReader(stringsReader(s)));
+}
+
+test(async function textprotoReader() {
+ let r = reader("line1\nline2\n");
+ let [s, err] = await r.readLine();
+ assertEqual(s, "line1");
+ assert(err == null);
+
+ [s, err] = await r.readLine();
+ assertEqual(s, "line2");
+ assert(err == null);
+
+ [s, err] = await r.readLine();
+ assertEqual(s, "");
+ assert(err == "EOF");
+});
+
+/*
+test(async function textprotoReadMIMEHeader() {
+ let r = reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n");
+ let [m, err] = await r.readMIMEHeader();
+
+ console.log("Got headers", m.toString());
+ want := MIMEHeader{
+ "My-Key": {"Value 1", "Value 2"},
+ "Long-Key": {"Even Longer Value"},
+ }
+ if !reflect.DeepEqual(m, want) || err != nil {
+ t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want)
+ }
+});
+*/
+
+test(async function textprotoReadMIMEHeaderSingle() {
+ let r = reader("Foo: bar\n\n");
+ let [m, err] = await r.readMIMEHeader();
+ assertEqual(m.get("Foo"), "bar");
+ assert(!err);
+});
+
+// Test that we read slightly-bogus MIME headers seen in the wild,
+// with spaces before colons, and spaces in keys.
+test(async function textprotoReadMIMEHeaderNonCompliant() {
+ // Invalid HTTP response header as sent by an Axis security
+ // camera: (this is handled by IE, Firefox, Chrome, curl, etc.)
+ let r = reader(
+ "Foo: bar\r\n" +
+ "Content-Language: en\r\n" +
+ "SID : 0\r\n" +
+ "Audio Mode : None\r\n" +
+ "Privilege : 127\r\n\r\n"
+ );
+ let [m, err] = await r.readMIMEHeader();
+ console.log(m.toString());
+ assert(!err);
+ /*
+ let want = MIMEHeader{
+ "Foo": {"bar"},
+ "Content-Language": {"en"},
+ "Sid": {"0"},
+ "Audio Mode": {"None"},
+ "Privilege": {"127"},
+ }
+ if !reflect.DeepEqual(m, want) || err != nil {
+ t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want)
+ }
+ */
+});
+
+test(async function textprotoAppend() {
+ const enc = new TextEncoder();
+ const dec = new TextDecoder();
+ const u1 = enc.encode("Hello ");
+ const u2 = enc.encode("World");
+ const joined = append(u1, u2);
+ assertEqual(dec.decode(joined), "Hello World");
+});
diff --git a/net/util.ts b/net/util.ts
new file mode 100644
index 000000000..811940b4d
--- /dev/null
+++ b/net/util.ts
@@ -0,0 +1,29 @@
+import { Buffer, Reader } from "deno";
+
+export function assert(cond: boolean, msg = "assert") {
+ if (!cond) {
+ throw Error(msg);
+ }
+}
+
+// `off` is the offset into `dst` where it will at which to begin writing values
+// from `src`.
+// Returns the number of bytes copied.
+export function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number {
+ const r = dst.byteLength - off;
+ if (src.byteLength > r) {
+ src = src.subarray(0, r);
+ }
+ dst.set(src, off);
+ return src.byteLength;
+}
+
+export function charCode(s: string): number {
+ return s.charCodeAt(0);
+}
+
+const encoder = new TextEncoder();
+export function stringsReader(s: string): Reader {
+ const ui8 = encoder.encode(s);
+ return new Buffer(ui8.buffer as ArrayBuffer);
+}