Skip to main content
Module

std/http/server.ts

Deno standard library
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.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")) { // typeof r.body === "string" handled in writeResponse. 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"];
/** write trailer headers to writer. it mostly should be called after writeResponse */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; /** * 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: ServerRequestBody | 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(): ServerRequestBody { if (!this._body) { const stream = this._bodyStream(); stream.next(); // drop dummy such that first read is not empty. this._body = new ServerRequestBody(stream); } return this._body; }
/** * Internal: actually reading body. Each step, buf to use is passed * in through yield result. * Returns on no more data to read or error. */ private async *_bodyStream(): AsyncIterator<number, undefined, Uint8Array> { let buf = yield 0; // dummy yield to retrieve user provided buf. 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")) { // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 const tp = new TextProtoReader(this.r); let line = await tp.readLine(); if (line === Deno.EOF) throw new UnexpectedEOFError(); // TODO: handle chunk extension 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; // Since given readBuffer might be smaller, loop. while (currChunkOffset < chunkSize) { // Try to be as large as chunkSize. Might be smaller though. 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(); // Consume \r\n 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); } } /* 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... Do nothing } }
async respond(r: Response): Promise<void> { 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; } }}
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")) { // A sender MUST NOT send a Content-Length header field in any message // that contains a Transfer-Encoding header field. // rfc: https://tools.ietf.org/html/rfc7230#section-3.3.2 throw new Error( "http: Transfer-Encoding and Content-Length cannot be send together" ); } }}
/** * ParseHTTPVersion parses a HTTP version string. * "HTTP/1.0" returns (1, 0, true). * Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request.go#L766-L792 */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; // arbitrary upper bound const digitReg = /^\d+$/; // test if string is only digit
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(); // e.g. GET /index.html HTTP/1.0 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(); }
// Yields all HTTP requests on a single TCP connection. 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;
// 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; } }
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: encoder.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<ServerRequest> ): AsyncIterableIterator<ServerRequest> { 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<ServerRequest> { const mux: MuxAsyncIterator<ServerRequest> = new MuxAsyncIterator(); mux.add(this.acceptConnAndIterateHttpRequests(mux)); return mux.iterate(); }}
/** Options for creating an HTTP server. */export type HTTPOptions = Omit<Deno.ListenOptions, "transport">;
/** * 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<void> { const server = serve(addr);
for await (const request of server) { handler(request); }}
/** Options for creating an HTTPS server. */export type HTTPSOptions = Omit<Deno.ListenTLSOptions, "transport">;
/** * 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<void> { 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> | Headers;}