const { listen, listenTLS, copy, toAsyncIterator } = Deno;type Listener = Deno.Listener;type Conn = Deno.Conn;type Reader = Deno.Reader;type Writer = Deno.Writer;import { BufReader, BufWriter, UnexpectedEOFError } from "../io/bufio.ts";import { TextProtoReader } from "../textproto/mod.ts";import { STATUS_TEXT } from "./http_status.ts";import { assert } from "../testing/asserts.ts";import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts";
const encoder = new TextEncoder();
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.set("content-length", bodyLength.toString()); } else { r.headers.set("transfer-encoding", "chunked"); } } }}
async function writeChunkedBody(w: Writer, r: Reader): Promise<void> { const writer = BufWriter.create(w);
for await (const chunk of toAsyncIterator(r)) { if (chunk.byteLength <= 0) continue; const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); const end = encoder.encode("\r\n"); await writer.write(start); await writer.write(chunk); await writer.write(end); }
const endChunk = encoder.encode("0\r\n\r\n"); await writer.write(endChunk);}const kProhibitedTrailerHeaders = [ "transfer-encoding", "content-length", "trailer"];
export async function writeTrailers( w: Writer, headers: Headers, trailers: Headers): Promise<void> { const trailer = headers.get("trailer"); if (trailer === null) { throw new Error('response headers must have "trailer" header field'); } const transferEncoding = headers.get("transfer-encoding"); if (transferEncoding === null || !transferEncoding.match(/^chunked/)) { throw new Error( `trailer headers is only allowed for "transfer-encoding: chunked": got "${transferEncoding}"` ); } const writer = BufWriter.create(w); const trailerHeaderFields = trailer .split(",") .map(s => s.trim().toLowerCase()); for (const f of trailerHeaderFields) { assert( !kProhibitedTrailerHeaders.includes(f), `"${f}" is prohibited for trailer header` ); } for (const [key, value] of trailers) { assert( trailerHeaderFields.includes(key), `Not trailer header field: ${key}` ); await writer.write(encoder.encode(`${key}: ${value}\r\n`)); } await writer.write(encoder.encode("\r\n")); await writer.flush();}
export async function writeResponse(w: Writer, r: Response): Promise<void> { const protoMajor = 1; const protoMinor = 1; const statusCode = r.status || 200; const statusText = STATUS_TEXT.get(statusCode); const writer = BufWriter.create(w); if (!statusText) { throw Error("bad status code"); } if (!r.body) { r.body = new Uint8Array(); } if (typeof r.body === "string") { r.body = encoder.encode(r.body); }
let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`;
setContentLength(r); assert(r.headers != null); const headers = r.headers;
for (const [key, value] of headers) { out += `${key}: ${value}\r\n`; } out += "\r\n";
const header = encoder.encode(out); const n = await writer.write(header); assert(n === header.byteLength);
if (r.body instanceof Uint8Array) { const n = await writer.write(r.body); assert(n === r.body.byteLength); } else if (headers.has("content-length")) { const contentLength = headers.get("content-length"); assert(contentLength != null); const bodyLength = parseInt(contentLength); const n = await copy(writer, r.body); assert(n === bodyLength); } else { await writeChunkedBody(writer, r.body); } if (r.trailers) { const t = await r.trailers(); await writeTrailers(writer, headers, t); } await writer.flush();}
export class ServerRequestBody implements Reader { constructor(private it: AsyncIterator<number, undefined, Uint8Array>) {} async read(p: Uint8Array): Promise<number | Deno.EOF> { const res = await this.it.next(p); if (res.done) { return Deno.EOF; } return res.value; }}
export class ServerRequest { url!: string; method!: string; proto!: string; protoMinor!: number; protoMajor!: number; headers!: Headers; conn!: Conn; r!: BufReader; w!: BufWriter; done: Deferred<Error | undefined> = deferred();
private _contentLength: number | undefined | null = undefined; get contentLength(): number | null { if (this._contentLength === undefined) { const cl = this.headers.get("content-length"); if (cl) { this._contentLength = parseInt(cl); if (Number.isNaN(this._contentLength)) { this._contentLength = null; } } else { this._contentLength = null; } } return this._contentLength; }
private _body: ServerRequestBody | null = null;
get body(): ServerRequestBody { if (!this._body) { const stream = this._bodyStream(); stream.next(); this._body = new ServerRequestBody(stream); } return this._body; }
private async *_bodyStream(): AsyncIterator<number, undefined, Uint8Array> { let buf = yield 0; if (this.headers.has("content-length")) { const len = this.contentLength; if (len === null) { return; } let rr = await this.r.read(buf); let nread = rr === Deno.EOF ? 0 : rr; let nreadTotal = nread; while (rr !== Deno.EOF && nreadTotal < len) { buf = yield nread; rr = await this.r.read(buf); nread = rr === Deno.EOF ? 0 : rr; nreadTotal += nread; } yield nread; } else { const transferEncoding = this.headers.get("transfer-encoding"); if (transferEncoding) { const parts = transferEncoding .split(",") .map((e): string => e.trim().toLowerCase()); if (parts.includes("chunked")) { const tp = new TextProtoReader(this.r); let line = await tp.readLine(); if (line === Deno.EOF) throw new UnexpectedEOFError(); const [chunkSizeString] = line.split(";"); let chunkSize = parseInt(chunkSizeString, 16); if (Number.isNaN(chunkSize) || chunkSize < 0) { throw new Error("Invalid chunk size"); } while (chunkSize > 0) { let currChunkOffset = 0; while (currChunkOffset < chunkSize) { const bufferToFill = buf.subarray(0, chunkSize); if ((await this.r.readFull(bufferToFill)) === Deno.EOF) { throw new UnexpectedEOFError(); } currChunkOffset += bufferToFill.length; buf = yield bufferToFill.length; } await this.r.readLine(); line = await tp.readLine(); if (line === Deno.EOF) throw new UnexpectedEOFError(); chunkSize = parseInt(line, 16); } const entityHeaders = await tp.readMIMEHeader(); if (entityHeaders !== Deno.EOF) { for (const [k, v] of entityHeaders) { this.headers.set(k, v); } } return; } } } }
async respond(r: Response): Promise<void> { let err: Error | undefined; try { await writeResponse(this.w, r); } catch (e) { try { this.conn.close(); } catch {} err = e; } this.done.resolve(err); if (err) { throw err; } }}
function fixLength(req: ServerRequest): void { const contentLength = req.headers.get("Content-Length"); if (contentLength) { const arrClen = contentLength.split(","); if (arrClen.length > 1) { const distinct = [...new Set(arrClen.map((e): string => e.trim()))]; if (distinct.length > 1) { throw Error("cannot contain multiple Content-Length headers"); } else { req.headers.set("Content-Length", distinct[0]); } } const c = req.headers.get("Content-Length"); if (req.method === "HEAD" && c && c !== "0") { throw Error("http: method cannot contain a Content-Length"); } if (c && req.headers.has("transfer-encoding")) { throw new Error( "http: Transfer-Encoding and Content-Length cannot be send together" ); } }}
export function parseHTTPVersion(vers: string): [number, number] { switch (vers) { case "HTTP/1.1": return [1, 1];
case "HTTP/1.0": return [1, 0];
default: { const Big = 1000000; const digitReg = /^\d+$/;
if (!vers.startsWith("HTTP/")) { break; }
const dot = vers.indexOf("."); if (dot < 0) { break; }
const majorStr = vers.substring(vers.indexOf("/") + 1, dot); const major = parseInt(majorStr); if ( !digitReg.test(majorStr) || isNaN(major) || major < 0 || major > Big ) { break; }
const minorStr = vers.substring(dot + 1); const minor = parseInt(minorStr); if ( !digitReg.test(minorStr) || isNaN(minor) || minor < 0 || minor > Big ) { break; }
return [major, minor]; } }
throw new Error(`malformed HTTP version ${vers}`);}
export async function readRequest( conn: Conn, bufr: BufReader): Promise<ServerRequest | Deno.EOF> { const tp = new TextProtoReader(bufr); const firstLine = await tp.readLine(); if (firstLine === Deno.EOF) return Deno.EOF; const headers = await tp.readMIMEHeader(); if (headers === Deno.EOF) throw new UnexpectedEOFError();
const req = new ServerRequest(); req.conn = conn; req.r = bufr; [req.method, req.url, req.proto] = firstLine.split(" ", 3); [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); req.headers = headers; fixLength(req); return req;}
export class Server implements AsyncIterable<ServerRequest> { private closing = false;
constructor(public listener: Listener) {}
close(): void { this.closing = true; this.listener.close(); }
private async *iterateHttpRequests( conn: Conn ): AsyncIterableIterator<ServerRequest> { const bufr = new BufReader(conn); const w = new BufWriter(conn); let req: ServerRequest | Deno.EOF | undefined; let err: Error | undefined;
while (!this.closing) { try { req = await readRequest(conn, bufr); } catch (e) { err = e; break; } if (req === Deno.EOF) { break; }
req.w = w; yield req;
const procError = await req.done; if (procError) { return; } }
if (req === Deno.EOF) { } else if (err && req) { try { await writeResponse(req.w, { status: 400, body: encoder.encode(`${err.message}\r\n\r\n`) }); } catch (_) { } } else if (this.closing) { }
conn.close(); }
private async *acceptConnAndIterateHttpRequests( mux: MuxAsyncIterator<ServerRequest> ): AsyncIterableIterator<ServerRequest> { if (this.closing) return; const { value, done } = await this.listener.next(); if (done) return; const conn = value as Conn; mux.add(this.acceptConnAndIterateHttpRequests(mux)); yield* this.iterateHttpRequests(conn); }
[Symbol.asyncIterator](): AsyncIterableIterator<ServerRequest> { const mux: MuxAsyncIterator<ServerRequest> = new MuxAsyncIterator(); mux.add(this.acceptConnAndIterateHttpRequests(mux)); return mux.iterate(); }}
export type HTTPOptions = Omit<Deno.ListenOptions, "transport">;
export function serve(addr: string | HTTPOptions): Server { if (typeof addr === "string") { const [hostname, port] = addr.split(":"); addr = { hostname, port: Number(port) }; }
const listener = listen(addr); return new Server(listener);}
export async function listenAndServe( addr: string | HTTPOptions, handler: (req: ServerRequest) => void): Promise<void> { const server = serve(addr);
for await (const request of server) { handler(request); }}
export type HTTPSOptions = Omit<Deno.ListenTLSOptions, "transport">;
export function serveTLS(options: HTTPSOptions): Server { const tlsOptions: Deno.ListenTLSOptions = { ...options, transport: "tcp" }; const listener = listenTLS(tlsOptions); return new Server(listener);}
export async function listenAndServeTLS( options: HTTPSOptions, handler: (req: ServerRequest) => void): Promise<void> { const server = serveTLS(options);
for await (const request of server) { handler(request); }}
export interface Response { status?: number; headers?: Headers; body?: Uint8Array | Reader | string; trailers?: () => Promise<Headers> | Headers;}