diff --git a/bufio.ts b/bufio.ts index 2ad9bdd09b..819c610f94 100644 --- a/bufio.ts +++ b/bufio.ts @@ -12,7 +12,13 @@ const MAX_CONSECUTIVE_EMPTY_READS = 100; const CR = charCode("\r"); const LF = charCode("\n"); -export type BufState = null | "EOF" | "BufferFull" | "NoProgress" | Error; +export type BufState = + | null + | "EOF" + | "BufferFull" + | "ShortWrite" + | "NoProgress" + | Error; /** BufReader implements buffering for a Reader object. */ export class BufReader implements Reader { @@ -102,7 +108,7 @@ export class BufReader implements Reader { * 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: ArrayBufferView): Promise { + async read(p: Uint8Array): Promise { let rr: ReadResult = { nread: p.byteLength, eof: false }; if (rr.nread === 0) { if (this.err) { @@ -334,7 +340,7 @@ export class BufReader implements Reader { export class BufWriter implements Writer { buf: Uint8Array; n: number = 0; - err: null | Error = null; + err: null | BufState = null; constructor(private wr: Writer, size = DEFAULT_BUF_SIZE) { if (size <= 0) { @@ -358,16 +364,16 @@ export class BufWriter implements Writer { } /** Flush writes any buffered data to the underlying io.Writer. */ - async flush(): Promise { + async flush(): Promise { if (this.err != null) { - throw this.err; + return this.err; } if (this.n == 0) { - return; + return null; } let n: number; - let err: Error = null; + let err: BufState = null; try { n = await this.wr.write(this.buf.subarray(0, this.n)); } catch (e) { @@ -375,7 +381,7 @@ export class BufWriter implements Writer { } if (n < this.n && err == null) { - err = new Error("ShortWrite"); + err = "ShortWrite"; } if (err != null) { @@ -384,7 +390,7 @@ export class BufWriter implements Writer { } this.n -= n; this.err = err; - return; + return err; } this.n = 0; } diff --git a/headers.ts b/headers.ts deleted file mode 100644 index 9fe2181950..0000000000 --- a/headers.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Fake headers to work around -// https://github.com/denoland/deno/issues/1173 - -function normalize(name: string, value?: string): [string, string] { - name = String(name).toLowerCase(); - value = String(value).trim(); - return [name, value]; -} - -export class Headers { - private map = new Map(); - - get(name: string): string | null { - let [name_] = normalize(name); - return this.map.get(name_); - } - - append(name: string, value: string): void { - [name, value] = normalize(name, value); - this.map.set(name, value); - } - - toString(): string { - let out = ""; - this.map.forEach((v, k) => { - out += `${k}: ${v}\n`; - }); - return out; - } - - [Symbol.iterator](): IterableIterator<[string, string]> { - return this.map[Symbol.iterator](); - } -} diff --git a/http.ts b/http.ts index 0266501ae3..4a4f0ccd95 100644 --- a/http.ts +++ b/http.ts @@ -1,7 +1,8 @@ import { listen, Conn } from "deno"; -import { BufReader, BufState } from "./bufio.ts"; +import { BufReader, BufState, BufWriter } from "./bufio.ts"; import { TextProtoReader } from "./textproto.ts"; -import { Headers } from "./headers.ts"; +import { STATUS_TEXT } from "./http_status"; +import { assert } from "./util"; export async function* serve(addr: string) { const listener = listen("tcp", addr); @@ -14,9 +15,21 @@ export async function* serve(addr: string) { export async function* serveConn(c: Conn) { let bufr = new BufReader(c); + let bufw = new BufWriter(c); try { while (true) { - const req = await readRequest(bufr); + const [req, err] = await readRequest(bufr); + if (err == "EOF") { + break; + } + if (err == "ShortWrite") { + console.log("ShortWrite error"); + break; + } + if (err) { + throw err; + } + req.w = bufw; yield req; } } finally { @@ -26,7 +39,19 @@ export async function* serveConn(c: Conn) { interface Response { status?: number; - body: string; + headers?: Headers; + body?: Uint8Array; +} + +function setContentLength(r: Response): void { + if (r.body) { + if (!r.headers) { + r.headers = new Headers(); + } + if (!r.headers.has("content-length")) { + r.headers.append("Content-Length", r.body.byteLength.toString()); + } + } } class ServerRequest { @@ -34,13 +59,41 @@ class ServerRequest { method: string; proto: string; headers: Headers; + w: BufWriter; - respond(r: Response): Promise { - throw Error("not implemented"); + async respond(r: Response): Promise { + 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} ${r.status} ${statusText}\r\n`; + + setContentLength(r); + + if (r.headers) { + for (let [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) { + n = await this.w.write(r.body); + assert(r.body.byteLength == n); + } + + await this.w.flush(); } } -async function readRequest(b: BufReader): Promise { +async function readRequest(b: BufReader): Promise<[ServerRequest, BufState]> { const tp = new TextProtoReader(b); const req = new ServerRequest(); @@ -49,9 +102,12 @@ async function readRequest(b: BufReader): Promise { // 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; + return [req, err]; } diff --git a/http_status.ts b/http_status.ts new file mode 100644 index 0000000000..a3006d319b --- /dev/null +++ b/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.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_test.ts b/http_test.ts index 1b16b0f0a1..b0007a8927 100644 --- a/http_test.ts +++ b/http_test.ts @@ -5,10 +5,11 @@ const addr = "0.0.0.0:8000"; const s = serve(addr); console.log(`listening on http://${addr}/`); +const body = new TextEncoder().encode("Hello World\n"); + async function main() { for await (const req of s) { - console.log("Req", req); - req.respond({ body: "Hello World\n" }); + await req.respond({ status: 200, body }); } } diff --git a/textproto.ts b/textproto.ts index 61ca45a8ad..50c0fea9cd 100644 --- a/textproto.ts +++ b/textproto.ts @@ -5,7 +5,6 @@ import { BufReader, BufState } from "./bufio.ts"; import { charCode } from "./util.ts"; -import { Headers } from "./headers.ts"; const asciiDecoder = new TextDecoder("ascii"); function str(buf: Uint8Array): string {