// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. import { BufReader, BufWriter } from "../io/bufio.ts"; import { assert } from "../testing/asserts.ts"; import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts"; import { bodyReader, chunkedBodyReader, emptyReader, writeResponse, readRequest } from "./io.ts"; import { encode } from "../strings/mod.ts"; import Listener = Deno.Listener; import Conn = Deno.Conn; import Reader = Deno.Reader; const { listen, listenTLS } = Deno; export class ServerRequest { url!: string; method!: string; proto!: string; protoMinor!: number; protoMajor!: number; headers!: Headers; conn!: Conn; r!: BufReader; w!: BufWriter; done: Deferred = deferred(); private _contentLength: number | undefined | null = undefined; /** * Value of Content-Length header. * If null, then content length is invalid or not given (e.g. chunked encoding). */ get contentLength(): number | null { // undefined means not cached. // null means invalid or not provided. if (this._contentLength === undefined) { const cl = this.headers.get("content-length"); if (cl) { this._contentLength = parseInt(cl); // Convert NaN to null (as NaN harder to test) if (Number.isNaN(this._contentLength)) { this._contentLength = null; } } else { this._contentLength = null; } } return this._contentLength; } private _body: Deno.Reader | null = null; /** * Body of the request. * * const buf = new Uint8Array(req.contentLength); * let bufSlice = buf; * let totRead = 0; * while (true) { * const nread = await req.body.read(bufSlice); * if (nread === Deno.EOF) break; * totRead += nread; * if (totRead >= req.contentLength) break; * bufSlice = bufSlice.subarray(nread); * } */ get body(): Deno.Reader { if (!this._body) { if (this.contentLength != null) { this._body = bodyReader(this.contentLength, this.r); } else { const transferEncoding = this.headers.get("transfer-encoding"); if (transferEncoding != null) { const parts = transferEncoding .split(",") .map((e): string => e.trim().toLowerCase()); assert( parts.includes("chunked"), 'transfer-encoding must include "chunked" if content-length is not set' ); this._body = chunkedBodyReader(this.headers, this.r); } else { // Neither content-length nor transfer-encoding: chunked this._body = emptyReader(); } } } return this._body; } async respond(r: Response): Promise { let err: Error | undefined; try { // Write our response! await writeResponse(this.w, r); } catch (e) { try { // Eagerly close on error. this.conn.close(); } catch {} err = e; } // Signal that this request has been processed and the next pipelined // request on the same connection can be accepted. this.done.resolve(err); if (err) { // Error during responding, rethrow. throw err; } } private finalized = false; async finalize(): Promise { if (this.finalized) return; // Consume unread body const body = this.body; const buf = new Uint8Array(1024); while ((await body.read(buf)) !== Deno.EOF) {} this.finalized = true; } } export class Server implements AsyncIterable { private closing = false; constructor(public listener: Listener) {} close(): void { this.closing = true; this.listener.close(); } // Yields all HTTP requests on a single TCP connection. private async *iterateHttpRequests( conn: Conn ): AsyncIterableIterator { 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; // Wait for the request to be processed before we accept a new request on // this connection. const procError = await req.done; if (procError) { // Something bad happened during response. // (likely other side closed during pipelined req) // req.done implies this connection already closed, so we can just return. return; } // Consume unread body and trailers if receiver didn't consume those data await req.finalize(); } if (req === Deno.EOF) { // The connection was gracefully closed. } else if (err && req) { // An error was thrown while parsing request headers. try { await writeResponse(req.w, { status: 400, body: encode(`${err.message}\r\n\r\n`) }); } catch (_) { // The connection is destroyed. // Ignores the error. } } else if (this.closing) { // There are more requests incoming but the server is closing. // TODO(ry): send a back a HTTP 503 Service Unavailable status. } conn.close(); } // Accepts a new TCP connection and yields all HTTP requests that arrive on // it. When a connection is accepted, it also creates a new iterator of the // same kind and adds it to the request multiplexer so that another TCP // connection can be accepted. private async *acceptConnAndIterateHttpRequests( mux: MuxAsyncIterator ): AsyncIterableIterator { if (this.closing) return; // Wait for a new connection. const { value, done } = await this.listener.next(); if (done) return; const conn = value as Conn; // Try to accept another connection and add it to the multiplexer. mux.add(this.acceptConnAndIterateHttpRequests(mux)); // Yield the requests that arrive on the just-accepted connection. yield* this.iterateHttpRequests(conn); } [Symbol.asyncIterator](): AsyncIterableIterator { const mux: MuxAsyncIterator = new MuxAsyncIterator(); mux.add(this.acceptConnAndIterateHttpRequests(mux)); return mux.iterate(); } } /** Options for creating an HTTP server. */ export type HTTPOptions = Omit; /** * Start a HTTP server * * import { serve } from "https://deno.land/std/http/server.ts"; * const body = "Hello World\n"; * const s = serve({ port: 8000 }); * for await (const req of s) { * req.respond({ body }); * } */ 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 { const server = serve(addr); for await (const request of server) { handler(request); } } /** Options for creating an HTTPS server. */ export type HTTPSOptions = Omit; /** * Create an HTTPS server with given options * * const body = "Hello HTTPS"; * const options = { * hostname: "localhost", * port: 443, * certFile: "./path/to/localhost.crt", * keyFile: "./path/to/localhost.key", * }; * for await (const req of serveTLS(options)) { * req.respond({ body }); * } * * @param options Server configuration * @return Async iterable server instance for incoming requests */ export function serveTLS(options: HTTPSOptions): Server { const tlsOptions: Deno.ListenTLSOptions = { ...options, transport: "tcp" }; const listener = listenTLS(tlsOptions); return new Server(listener); } /** * Create an HTTPS server with given options and request handler * * const body = "Hello HTTPS"; * const options = { * hostname: "localhost", * port: 443, * certFile: "./path/to/localhost.crt", * keyFile: "./path/to/localhost.key", * }; * listenAndServeTLS(options, (req) => { * req.respond({ body }); * }); * * @param options Server configuration * @param handler Request handler */ export async function listenAndServeTLS( options: HTTPSOptions, handler: (req: ServerRequest) => void ): Promise { const server = serveTLS(options); for await (const request of server) { handler(request); } } /** * Interface of HTTP server response. * If body is a Reader, response would be chunked. * If body is a string, it would be UTF-8 encoded by default. */ export interface Response { status?: number; headers?: Headers; body?: Uint8Array | Reader | string; trailers?: () => Promise | Headers; }