Skip to main content
Module

x/opine/src/request.ts

Fast, minimalist web framework for Deno ported from ExpressJS.
Very Popular
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
import { Accepts, isIP, parseRange, readableStreamFromReader, readerFromStreamReader, typeofrequest,} from "../deps.ts";import type { ConnInfo } from "../deps.ts";import { fresh } from "./utils/fresh.ts";import { parseUrl } from "./utils/parseUrl.ts";import { all, proxyaddr } from "./utils/proxyAddr.ts";import { Application, NextFunction, OpineRequest, OpineResponse, ParamsDictionary, ParsedURL, RangeParserOptions, RangeParserRanges, RangeParserResult,} from "../src/types.ts";
function emptyReader(): Deno.Reader { return { read(_: Uint8Array): Promise<number | null> { return Promise.resolve(null); }, };}
const isDenoReader = (body: unknown): body is Deno.Reader => body !== null && typeof body === "object" && typeof (body as Deno.Reader).read === "function";
export class WrappedRequest implements OpineRequest { #request: Request; #connInfo: ConnInfo; #responsePromise: Promise<Response>; #responsePromiseResolver!: (response: Response) => void;
app!: Application; res!: OpineResponse; params!: ParamsDictionary; query: unknown; route: unknown;
url: string; originalUrl!: string; baseUrl!: string; proto: string;
method: string; headers: Headers;
parsedBody?: unknown; _parsedBody?: boolean | undefined; _parsedUrl?: ParsedURL | undefined; _parsedOriginalUrl?: ParsedURL | undefined;
next?: NextFunction | undefined; param!: ( name: string, defaultValue?: string | undefined, ) => string | undefined;
constructor(request: Request, connInfo: ConnInfo) { this.#request = request; this.#connInfo = connInfo; this.#responsePromise = new Promise((resolve) => { this.#responsePromiseResolver = resolve; });
const { pathname, search, hash } = new URL(request.url); this.url = `${pathname}${search}${hash}`; this.proto = parseUrl(this)?.protocol ?? ""; this.method = request.method; this.headers = new Headers(this.#request.headers); }
respond(response: OpineResponse) { const bodyInit = isDenoReader(response.body) ? readableStreamFromReader(response.body) : response.body;
this.#responsePromiseResolver(new Response(bodyInit, response)); }
/** * Check if the given `type(s)` is acceptable, returning * the best match when true, otherwise `undefined`, in which * case you should respond with 406 "Not Acceptable". * * The `type` value may be a single MIME type string * such as "application/json", an extension name * such as "json", a comma-delimited list such as "json, html, text/plain", * an argument list such as `"json", "html", "text/plain"`, * or an array `["json", "html", "text/plain"]`. When a list * or array is given, the _best_ match, if any is returned. * * Examples: * * // Accept: text/html * req.accepts('html'); * // => "html" * * // Accept: text/*, application/json * req.accepts('html'); * // => "html" * req.accepts('text/html'); * // => "text/html" * req.accepts('json, text'); * // => "json" * req.accepts('application/json'); * // => "application/json" * * // Accept: text/*, application/json * req.accepts('image/png'); * req.accepts('png'); * // => undefined * * // Accept: text/*;q=.5, application/json * req.accepts(['html', 'json']); * req.accepts('html', 'json'); * req.accepts('html, json'); * // => "json" * * @param {string|string[]} type * @return {string|string[]|false} * @public */ accepts(this: OpineRequest, ...args: [string[]] | string[]) { const accept = new Accepts(this.headers);
return accept.types.call(accept, args.flat(1)); }
/** * Check if the given `charset`s are acceptable, * otherwise you should respond with 406 "Not Acceptable". * * @param {string|string[]} ...charset * @return {string|string[]|false} * @public */ acceptsCharsets( this: OpineRequest, ...args: [string[]] | string[] ) { const accept = new Accepts(this.headers);
return accept.charsets.call(accept, args.flat(1)); }
/** * Check if the given `encoding`s are accepted. * * @param {string|string[]} ...encoding * @return {string|string[]|false} * @public */
acceptsEncodings( this: OpineRequest, ...args: [string[]] | string[] ) { const accept = new Accepts(this.headers);
return accept.encodings.call(accept, args.flat(1)); }
/** * Check if the given `lang`s are acceptable, * otherwise you should respond with 406 "Not Acceptable". * * @param {string|string[]} ...lang * @return {string|string[]|false} * @public */ acceptsLanguages( this: OpineRequest, ...args: [string[]] | string[] ) { const accept = new Accepts(this.headers);
return accept.languages.call(accept, args.flat(1)); }
/** * Return request header. * * The `Referrer` header field is special-cased, * both `Referrer` and `Referer` are interchangeable. * * Examples: * * req.get('Content-Type'); * // => "text/plain" * * req.get('content-type'); * // => "text/plain" * * req.get('Something'); * // => undefined * * Aliased as `req.header()`. * * @param {string} name * @return {string} * @public */ get(name: string): string | undefined { const lc = name.toLowerCase();
switch (lc) { case "referer": case "referrer": return this.headers.get("referrer") || this.headers.get("referer") || undefined; default: return this.headers.get(lc) || undefined; } }
/** * Parse Range header field, capping to the given `size`. * * Unspecified ranges such as "0-" require knowledge of your resource length. In * the case of a byte range this is of course the total number of bytes. If the * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, * and `-2` when syntactically invalid. * * When ranges are returned, the array has a "type" property which is the type of * range that is required (most commonly, "bytes"). Each array element is an object * with a "start" and "end" property for the portion of the range. * * The "combine" option can be set to `true` and overlapping & adjacent ranges * will be combined into a single range. * * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" * should respond with 4 users when available, not 3. * * @param {number} size * @param {object} [options] * @param {boolean} [options.combine=false] * @return {number|number[]|undefined} * @public */ range( size: number, options?: RangeParserOptions, ): RangeParserRanges | RangeParserResult | undefined { const range = this.get("Range");
if (!range) return;
return parseRange(size, range, options) as | RangeParserRanges | RangeParserResult; }
/** * Check if the incoming request contains the "Content-Type" * header field, and it contains the give mime `type`. * * Examples: * * // With Content-Type: text/html; charset=utf-8 * req.is('html'); * req.is('text/html'); * req.is('text/*'); * // => true * * // When Content-Type is application/json * req.is('json'); * req.is('application/json'); * req.is('application/*'); * // => true * * req.is('html'); * // => false * * @param {String|Array} types... * @return {String|false|null} * @public */ is(this: OpineRequest, types: string | string[]) { let arr = types;
// support flattened arguments if (!Array.isArray(types)) { arr = new Array(arguments.length); for (let i = 0; i < arr.length; i++) { arr[i] = arguments[i]; } }
return typeofrequest(this.headers, arr as string[]); }
upgrade(): WebSocket { const { socket, response } = Deno.upgradeWebSocket(this.#request); this.#responsePromiseResolver(response); return socket; }
get #body() { const streamReader = this.#request.body?.getReader(); return streamReader ? readerFromStreamReader(streamReader) : emptyReader(); }
get body() { return this.parsedBody ?? this.#body; }
set body(value: unknown) { this.parsedBody = value; }
get raw() { return this.#body; }
get conn() { return this.#connInfo; }
/** * Returns a promise that resolves to the response to the request. */ get finalResponse() { return this.#responsePromise; }
/** * Return the protocol string "http" or "https" * when requested with TLS. When the "trust proxy" * setting trusts the socket address, the * "X-Forwarded-Proto" header field will be trusted * and used if present. * * If you're running behind a reverse proxy that * supplies https for you this may be enabled. * * @return {string} * @public */ get protocol() { const proto = this.proto.includes("https") ? "https" : "http"; const trust = this.app.get("trust proxy fn"); const { hostname: remoteAddress } = this.conn .remoteAddr as Deno.NetAddr;
if (!trust(remoteAddress, 0)) { return proto; }
// Note: X-Forwarded-Proto is normally only ever a // single value, but this is to be safe. const header = this.get("X-Forwarded-Proto") ?? proto; const index = header.indexOf(",");
return index !== -1 ? header.substring(0, index).trim() : header.trim(); }
/** * Short-hand for: * * req.protocol === 'https' * * @return {Boolean} * @public */ get secure() { return this.protocol === "https"; }
/** * Return the remote address from the trusted proxy. * * The is the remote address on the socket unless * "trust proxy" is set. * * @return {string} * @public */
get ip(): string { const trust = this.app.get("trust proxy fn");
return proxyaddr(this, trust); }
/** * When "trust proxy" is set, trusted proxy addresses + client. * * For example if the value were "client, proxy1, proxy2" * you would receive the array `["client", "proxy1", "proxy2"]` * where "proxy2" is the furthest down-stream and "proxy1" and * "proxy2" were trusted. * * @return {Array} * @public */ get ips() { const trust = this.app.get("trust proxy fn"); const addrs = all(this, trust);
// Reverse the order (to farthest -> closest) // and remove socket address addrs.reverse().pop();
return addrs; }
/** * Return subdomains as an array. * * Subdomains are the dot-separated parts of the host before the main domain of * the app. By default, the domain of the app is assumed to be the last two * parts of the host. This can be changed by setting "subdomain offset". * * For example, if the domain is "deno.dinosaurs.example.com": * If "subdomain offset" is not set, req.subdomains is `["dinosaurs", "deno"]`. * If "subdomain offset" is 3, req.subdomains is `["deno"]`. * * @return {Array} * @public */ get subdomains() { const hostname = this.hostname;
if (!hostname) return [];
const offset = this.app.get("subdomain offset"); const subdomains = !isIP(hostname) ? hostname.split(".").reverse() : [hostname];
return subdomains.slice(offset); }
/** * Returns the pathname of the URL. * * @return {string} * @public */ get path(): string { return parseUrl(this)?.pathname ?? ""; }
/** * Parse the "Host" header field to a hostname. * * When the "trust proxy" setting trusts the socket * address, the "X-Forwarded-Host" header field will * be trusted. * * @return {string} * @public */ get hostname(): string | undefined { const trust = this.app.get("trust proxy fn"); let host = this.get("X-Forwarded-Host"); const { hostname: remoteAddress } = this.conn .remoteAddr as Deno.NetAddr;
if (!host || !trust(remoteAddress, 0)) { host = this.get("Host"); } else if (host.indexOf(",") !== -1) { // Note: X-Forwarded-Host is normally only ever a // single value, but this is to be safe. host = host.substring(0, host.indexOf(",")).trimRight(); }
if (!host) { return undefined; }
// IPv6 literal support const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; const index = host.indexOf(":", offset);
return index !== -1 ? host.substring(0, index) : host; }
/** * Check if the request is fresh, aka * Last-Modified and/or the ETag * still match. * * @return {boolean} * @public */ get fresh() { const method = this.method; const res = this.res as OpineResponse; const status = res.status as number;
// GET or HEAD for weak freshness validation only if ("GET" !== method && "HEAD" !== method) { return false; }
// 2xx or 304 as per rfc2616 14.26 if ((status >= 200 && status < 300) || 304 === status) { return fresh(Object.fromEntries(this.headers), { "etag": res.get("ETag"), "last-modified": res.get("Last-Modified"), }); }
return false; }
/** * Check if the request is stale, aka * "Last-Modified" and / or the "ETag" for the * resource has changed. * * @return {Boolean} * @public */ get stale(): boolean { return !this.fresh; }
/** * Check if the request was an _XMLHttpRequest_. * * @return {Boolean} * @public */ get xhr() { const val = this.get("X-Requested-With") || "";
return val.toLowerCase() === "xmlhttprequest"; }}