// Loaded from https://deno.land/x/oak@v6.3.1/application.ts // Copyright 2018-2020 the oak authors. All rights reserved. MIT license. import { Context } from "./context.ts"; import { serve as defaultServe, serveTLS as defaultServeTls, Status, STATUS_TEXT, } from "./deps.ts"; import { Key, KeyStack } from "./keyStack.ts"; import { compose, Middleware } from "./middleware.ts"; import type { Serve, Server, ServerRequest, ServerResponse, ServeTls, } from "./types.d.ts"; export interface ListenOptionsBase { hostname?: string; port: number; secure?: false; signal?: AbortSignal; } export interface ListenOptionsTls { certFile: string; hostname?: string; keyFile: string; port: number; secure: true; signal?: AbortSignal; } export type ListenOptions = ListenOptionsTls | ListenOptionsBase; function isOptionsTls(options: ListenOptions): options is ListenOptionsTls { return options.secure === true; } interface ApplicationErrorEventListener { (evt: ApplicationErrorEvent): void | Promise; } interface ApplicationErrorEventListenerObject { handleEvent(evt: ApplicationErrorEvent): void | Promise; } interface ApplicationErrorEventInit extends ErrorEventInit { context?: Context; } type ApplicationErrorEventListenerOrEventListenerObject = | ApplicationErrorEventListener | ApplicationErrorEventListenerObject; interface ApplicationListenEventListener { (evt: ApplicationListenEvent): void | Promise; } interface ApplicationListenEventListenerObject { handleEvent(evt: ApplicationListenEvent): void | Promise; } interface ApplicationListenEventInit extends EventInit { hostname?: string; port: number; secure: boolean; } type ApplicationListenEventListenerOrEventListenerObject = | ApplicationListenEventListener | ApplicationListenEventListenerObject; export interface ApplicationOptions { /** An initial set of keys (or instance of `KeyGrip`) to be used for signing * cookies produced by the application. */ keys?: KeyStack | Key[]; /** If set to `true`, proxy headers will be trusted when processing requests. * This defaults to `false`. */ proxy?: boolean; /** The `server()` function to be used to read requests. * * _Not used generally, as this is just for mocking for test purposes_ */ serve?: Serve; /** The `server()` function to be used to read requests. * * _Not used generally, as this is just for mocking for test purposes_ */ serveTls?: ServeTls; /** The initial state object for the application, of which the type can be * used to infer the type of the state for both the application and any of the * application's context. */ state?: S; } interface RequestState { handling: Set>; closing: boolean; closed: boolean; server: Server; } // deno-lint-ignore no-explicit-any export type State = Record; const ADDR_REGEXP = /^\[?([^\]]*)\]?:([0-9]{1,5})$/; export class ApplicationErrorEvent extends ErrorEvent { context?: Context; constructor(eventInitDict: ApplicationErrorEventInit) { super("error", eventInitDict); this.context = eventInitDict.context; } } export class ApplicationListenEvent extends Event { hostname?: string; port: number; secure: boolean; constructor(eventInitDict: ApplicationListenEventInit) { super("listen", eventInitDict); this.hostname = eventInitDict.hostname; this.port = eventInitDict.port; this.secure = eventInitDict.secure; } } /** A class which registers middleware (via `.use()`) and then processes * inbound requests against that middleware (via `.listen()`). * * The `context.state` can be typed via passing a generic argument when * constructing an instance of `Application`. */ // deno-lint-ignore no-explicit-any export class Application> extends EventTarget { #composedMiddleware?: (context: Context) => Promise; #keys?: KeyStack; #middleware: Middleware>[] = []; #serve: Serve; #serveTls: ServeTls; /** A set of keys, or an instance of `KeyStack` which will be used to sign * cookies read and set by the application to avoid tampering with the * cookies. */ get keys(): KeyStack | Key[] | undefined { return this.#keys; } set keys(keys: KeyStack | Key[] | undefined) { if (!keys) { this.#keys = undefined; return; } else if (Array.isArray(keys)) { this.#keys = new KeyStack(keys); } else { this.#keys = keys; } } /** If `true`, proxy headers will be trusted when processing requests. This * defaults to `false`. */ proxy: boolean; /** Generic state of the application, which can be specified by passing the * generic argument when constructing: * * const app = new Application<{ foo: string }>(); * * Or can be contextually inferred based on setting an initial state object: * * const app = new Application({ state: { foo: "bar" } }); * */ state: AS; constructor(options: ApplicationOptions = {}) { super(); const { state, keys, proxy, serve = defaultServe, serveTls = defaultServeTls, } = options; this.proxy = proxy ?? false; this.keys = keys; this.state = state ?? {} as AS; this.#serve = serve; this.#serveTls = serveTls; } #getComposed = (): ((context: Context) => Promise) => { if (!this.#composedMiddleware) { this.#composedMiddleware = compose(this.#middleware); } return this.#composedMiddleware; }; /** Deal with uncaught errors in either the middleware or sending the * response. */ // deno-lint-ignore no-explicit-any #handleError = (context: Context, error: any): void => { if (!(error instanceof Error)) { error = new Error(`non-error thrown: ${JSON.stringify(error)}`); } const { message } = error; this.dispatchEvent(new ApplicationErrorEvent({ context, message, error })); if (!context.response.writable) { return; } for (const key of context.response.headers.keys()) { context.response.headers.delete(key); } if (error.headers && error.headers instanceof Headers) { for (const [key, value] of error.headers) { context.response.headers.set(key, value); } } context.response.type = "text"; const status: Status = context.response.status = error instanceof Deno.errors.NotFound ? 404 : error.status && typeof error.status === "number" ? error.status : 500; context.response.body = error.expose ? error.message : STATUS_TEXT.get(status); }; /** Processing registered middleware on each request. */ #handleRequest = async ( request: ServerRequest, secure: boolean, state: RequestState, ): Promise => { const context = new Context(this, request, secure); let resolve: () => void; const handlingPromise = new Promise((res) => resolve = res); state.handling.add(handlingPromise); if (!state.closing && !state.closed) { try { await this.#getComposed()(context); } catch (err) { this.#handleError(context, err); } } if (context.respond === false) { context.response.destroy(); resolve!(); state.handling.delete(handlingPromise); return; } try { await request.respond(await context.response.toServerResponse()); if (state.closing) { state.server.close(); state.closed = true; } } catch (err) { this.#handleError(context, err); } finally { context.response.destroy(); resolve!(); state.handling.delete(handlingPromise); } }; /** Add an event listener for an `"error"` event which occurs when an * un-caught error occurs when processing the middleware or during processing * of the response. */ addEventListener( type: "error", listener: ApplicationErrorEventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void; /** Add an event listener for a `"listen"` event which occurs when the server * has successfully opened but before any requests start being processed. */ addEventListener( type: "listen", listener: ApplicationListenEventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void; /** Add an event listener for an event. Currently valid event types are * `"error"` and `"listen"`. */ addEventListener( type: "error" | "listen", listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void { super.addEventListener(type, listener, options); } /** Handle an individual server request, returning the server response. This * is similar to `.listen()`, but opening the connection and retrieving * requests are not the responsibility of the application. If the generated * context gets set to not to respond, then the method resolves with * `undefined`, otherwise it resolves with a request that is compatible with * `std/http/server`. */ handle = async ( request: ServerRequest, secure = false, ): Promise => { if (!this.#middleware.length) { throw new TypeError("There is no middleware to process requests."); } const context = new Context(this, request, secure); try { await this.#getComposed()(context); } catch (err) { this.#handleError(context, err); } if (context.respond === false) { context.response.destroy(); return; } try { const response = await context.response.toServerResponse(); context.response.destroy(); return response; } catch (err) { this.#handleError(context, err); throw err; } }; /** Start listening for requests, processing registered middleware on each * request. If the options `.secure` is undefined or `false`, the listening * will be over HTTP. If the options `.secure` property is `true`, a * `.certFile` and a `.keyFile` property need to be supplied and requests * will be processed over HTTPS. */ async listen(addr: string): Promise; /** Start listening for requests, processing registered middleware on each * request. If the options `.secure` is undefined or `false`, the listening * will be over HTTP. If the options `.secure` property is `true`, a * `.certFile` and a `.keyFile` property need to be supplied and requests * will be processed over HTTPS. */ async listen(options: ListenOptions): Promise; async listen(options: string | ListenOptions): Promise { if (!this.#middleware.length) { throw new TypeError("There is no middleware to process requests."); } if (typeof options === "string") { const match = ADDR_REGEXP.exec(options); if (!match) { throw TypeError(`Invalid address passed: "${options}"`); } const [, hostname, portStr] = match; options = { hostname, port: parseInt(portStr, 10) }; } const server = isOptionsTls(options) ? this.#serveTls(options) : this.#serve(options); const { signal } = options; const state = { closed: false, closing: false, handling: new Set>(), server, }; if (signal) { signal.addEventListener("abort", () => { if (!state.handling.size) { server.close(); state.closed = true; } state.closing = true; }); } const { hostname, port, secure = false } = options; this.dispatchEvent( new ApplicationListenEvent({ hostname, port, secure }), ); try { for await (const request of server) { this.#handleRequest(request, secure, state); } await Promise.all(state.handling); } catch (error) { const message = error instanceof Error ? error.message : "Application Error"; this.dispatchEvent( new ApplicationErrorEvent({ message, error }), ); } } /** Register middleware to be used with the application. Middleware will * be processed in the order it is added, but middleware can control the flow * of execution via the use of the `next()` function that the middleware * function will be called with. The `context` object provides information * about the current state of the application. * * Basic usage: * * ```ts * const import { Application } from "https://deno.land/x/oak/mod.ts"; * * const app = new Application(); * * app.use((ctx, next) => { * ctx.request; // contains request information * ctx.response; // setups up information to use in the response; * await next(); // manages the flow control of the middleware execution * }); * * await app.listen({ port: 80 }); * ``` */ use( ...middleware: Middleware>[] ): Application { this.#middleware.push(...middleware); this.#composedMiddleware = undefined; // deno-lint-ignore no-explicit-any return this as Application; } }