import { base64, http } from "./deps.ts";import { packBody, packJson, unpackBody } from "./pack.ts";import { HttpError, wrapWebSocket } from "./client.ts";
import type { Socket } from "./client.ts";import type { AnyRpc, Parser, ParserOutput } from "./rpc.ts";import type { AnyStack } from "./stack.ts";import type { Packers } from "./pack.ts";
export const NO_MATCH = new HttpError("404 not found", { status: 404 });
export interface RequestData { res: Headers; url: URL; path: string; groups: Record<string, string>; query: Record<string, string | string[]>;}
const _requestData = Symbol("_requestData");
export function requestData(request: Request): RequestData { const req = request as Request & Record<typeof _requestData, RequestData>; if (req[_requestData]) { return req[_requestData]; }
const url = new URL(req.url); const query: Record<string, string | string[]> = {}; url.searchParams.forEach((v, k) => { const old = query[k]; if (Array.isArray(old)) { query[k] = [...old, v]; } else if (typeof old === "string") { query[k] = [old, v]; } else { query[k] = v; } });
const data: RequestData = { res: new Headers(), url, path: url.pathname, groups: {}, query, }; Object.assign(req, { [_requestData]: data }); return data;}
const methodsWithBodies = new Set([ "POST", "PUT", "DELETE", "PATCH",]);
export async function requestBody(req: Request, opt?: { maxSize?: number; packers?: Packers;}): Promise<unknown> { if ( !req.body || req.bodyUsed || !methodsWithBodies.has(req.method) ) { return undefined; }
const maxSize = ( typeof opt?.maxSize === "number" ? opt.maxSize : 5 * 1024 * 1024 ); const length = parseInt(req.headers.get("content-length") || "", 10); if (isNaN(length)) { throw new HttpError("411 length required", { status: 411 }); } if (maxSize && length > maxSize) { throw new HttpError("413 payload too large", { status: 413 }); } return await unpackBody(req, opt?.packers);}
export interface Cookie { readonly original: { readonly [x: string]: string }; get(name: string, opt?: { signed?: boolean }): string | undefined; set(name: string, value: string, opt?: CookieSetOptions): void; delete(name: string, opt?: CookieDeleteOptions): void; signed(): [string, string][]; unsigned(): [string, string][]; flush(): Promise<void>;}
export interface CookieSetOptions extends Omit<http.Cookie, "name" | "value"> { signed?: boolean;}
export interface CookieDeleteOptions { path?: string; domain?: string;}
const random = new Uint8Array(32);crypto.getRandomValues(random);const decoder = new TextDecoder();const rand = decoder.decode(random);const fallbackKeys: [string, ...string[]] = [base64.encode(rand)];
export async function bakeCookie(init: { req: Request; res: Headers; keys?: [string, ...string[]];}): Promise<Cookie> { const keys = init.keys || fallbackKeys; const original = http.getCookies(init.req.headers); const unsigned = { ...original }; const signed: Record<string, string> = {};
for (const [k, v] of Object.entries(unsigned)) { const sig = unsigned[`${k}.sig`]; if (sig) { delete unsigned[`${k}.sig`]; delete unsigned[k]; if (await verify(v, sig, keys)) { signed[k] = v; } } }
const updates: ( | { op: "set", name: string, value: string, opt?: CookieSetOptions } | { op: "delete", name: string, opt?: CookieDeleteOptions } )[] = [];
const cookie: Cookie = { original, get(name, opt) { return opt?.signed ? signed[name] : unsigned[name]; }, set(name, value, opt) { updates.push({ op: "set", name, value, opt });
if (opt?.path || opt?.domain) { const p = new URLPattern({ hostname: opt.domain ? `{*.}?${opt.domain}` : "*", pathname: opt.path ? `${opt.path}/*?` : "*", }); if (!p.exec(init.req.url)) { return; } }
if (opt?.signed) { signed[name] = value; } else { unsigned[name] = value; } }, delete(name, opt) { updates.push({ op: "delete", name, opt }); if (signed[name]) { updates.push({ op: "delete", name: `${name}.sig`, opt }); } if (opt?.path || opt?.domain) { const p = new URLPattern({ hostname: opt.domain ? `{*.}?${opt.domain}` : "*", pathname: opt.path ? `${opt.path}/*?` : "*", }); if (!p.exec(init.req.url)) { return; } }
delete signed[name]; delete unsigned[name]; }, signed() { return Object.entries(signed); }, unsigned() { return Object.entries(unsigned); }, async flush() { let u = updates.shift(); while (u) { if (u.op === "delete") { http.deleteCookie(init.res, u.name, u.opt); }
if (u.op === "set") { http.setCookie(init.res, { ...u.opt, name: u.name, value: u.value, }); if (u.opt?.signed) { http.setCookie(init.res, { ...u.opt, name: `${u.name}.sig`, value: await sign(u.value, keys[0]), }); } }
u = updates.shift(); } }, };
return cookie;}
const keyCache = new Map<string, CryptoKey>();const encoder = new TextEncoder();const signingAlg = { name: "HMAC", hash: "SHA-256" } as const;
async function importKey(key: string): Promise<CryptoKey> { let k = keyCache.get(key); if (k) { return k; }
k = await crypto.subtle.importKey( "raw", encoder.encode(key), signingAlg, false, ["sign", "verify"], ); keyCache.set(key, k); return k;}
async function verify( data: string, sig: string, keys: [string, ...string[]],): Promise<boolean> { for (const key of keys) { const k = await importKey(key); if ( await crypto.subtle.verify( signingAlg, k, base64.decode(sig), encoder.encode(data), ) ) { return true; } } return false;}
async function sign(data: string, key: string): Promise<string> { const k = await importKey(key); return base64.encode( await crypto.subtle.sign( signingAlg, k, encoder.encode(data), ), );}
const _typedResponse = Symbol("_typedResponse");
export interface TypedResponse<T = unknown> extends Response { [_typedResponse]?: T; }
export type ResponseType<R extends Response> = ( R extends TypedResponse<infer T> ? T : unknown);
export interface TypedResponseInit extends ResponseInit { packers?: Packers;}
export function response<T = unknown>( body: T, init?: TypedResponseInit,): TypedResponse< T extends TypedResponse<infer T2> ? T2 : T extends Response ? unknown : T> { const headers = new Headers(init?.headers);
if (body instanceof Response) { for (const [k, v] of body.headers.entries()) { headers.append(k, v); } const statusText = ( !init?.status || body.status === init.status ? body.statusText : init?.statusText ); return new Response(body.body, { status: init?.status || body.status, statusText, headers, }) }
const { body: b, mime: m } = packBody(body, init?.packers); if (!headers.has("content-type")) { headers.append("content-type", m); } return new Response(b, { ...init, headers, });}
const _socketResponse = Symbol("_socketResponse");
export interface SocketResponse<O> extends Response { [_socketResponse]?: O; }
export function upgradeWebSocket< O extends unknown = unknown, IP extends Parser | undefined = undefined,>(req: Request, init?: { messageParser?: IP, packers?: Packers,}): { socket: Socket<O, IP extends Parser ? ParserOutput<IP> : unknown>, response: SocketResponse<O>,} { let raw: WebSocket; let response: SocketResponse<O>; try { const upgrade = Deno.upgradeWebSocket(req, { protocol: "json" }); raw = upgrade.socket; response = upgrade.response as SocketResponse<O>; } catch (e) { throw new HttpError("400 bad request", { status: 400, expose: { upgradeError: "Failed to upgrade web socket", reason: e instanceof Error ? e.message : "unknown", }, }); }
const socket = wrapWebSocket(raw, { parseMessage: async message => { if (init?.messageParser) { const parser = init.messageParser!; try { message = typeof parser === "function" ? await parser(message) : await parser.parse(message); } catch (e) { const error = new HttpError("400 bad request", { status: 400, expose: { messageError: e }, }); raw.send(packJson(error, init.packers)); throw undefined; } }
return message; }, });
return { socket, response };}
const _server = Symbol("_server");
export interface Server<H extends http.Handler> extends http.Server { [_server]?: H; }
export interface ServerInit< H extends http.Handler = http.Handler,> extends Omit<http.ServerInit, "onError"> { port?: number; handler: H;}
export function server<H extends http.Handler>( init: ServerInit<H>,): Server<H> { return new http.Server({ port: 8000, ...init, handler: async (req, conn) => { const data = requestData(req) let err: unknown = null; try { return await init.handler(req, conn); } catch (e) { err = e; }
if (err === NO_MATCH) { const e = err as HttpError; const headers = data.res; headers.append("content-length", e.message.length.toString()); headers.append("content-type", "text/plain"); return new Response(req.method === "HEAD" ? null : e.message, { status: e.status, headers: data.res, }); }
const bugtrace = crypto.randomUUID().slice(0, 8); console.error( `Error [${bugtrace}]: Uncaught exception during "${req.method} ${req.url}" -`, err, ); const body = `500 internal server error [${bugtrace}]`; data.res.append("content-length", body.length.toString()); return new Response(req.method === "HEAD" ? null : body, { status: ( err instanceof HttpError && err.status >= 500 ? err.status : 500 ), headers: data.res, }); }, });}
export async function serve( handler: AnyStack | AnyRpc, init?: Omit<ServerInit<http.Handler>, "handler">,): Promise<void> { return await server({ ...init, handler }).listenAndServe();}