UNPKG

@adonisjs/http-server

Version:

AdonisJS HTTP server with support packed with Routing and Cookies

1,851 lines (1,833 loc) 127 kB
var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/router/route.ts import is from "@sindresorhus/is"; import Macroable4 from "@poppinss/macroable"; import Middleware from "@poppinss/middleware"; import { RuntimeException as RuntimeException2 } from "@poppinss/utils"; import { moduleCaller, moduleImporter } from "@adonisjs/fold"; // src/router/factories/use_return_value.ts function useReturnValue(ctx) { return function(value) { if (value !== void 0 && // Return value is explicitly defined !ctx.response.hasLazyBody && // Lazy body is not set value !== ctx.response) { ctx.response.send(value); } }; } // src/router/executor.ts function execute(route, resolver, ctx, errorResponder) { return route.middleware.runner().errorHandler((error) => errorResponder(error, ctx)).finalHandler(async () => { if (typeof route.handler === "function") { return Promise.resolve(route.handler(ctx)).then(useReturnValue(ctx)); } return route.handler.handle(resolver, ctx).then(useReturnValue(ctx)); }).run(async (middleware, next) => { if (typeof middleware === "function") { return middleware(ctx, next); } return middleware.handle(resolver, ctx, next, middleware.args); }); } // src/helpers.ts import Cache from "tmp-cache"; import { InvalidArgumentsException } from "@poppinss/utils"; // src/router/brisk.ts import Macroable from "@poppinss/macroable"; var BriskRoute = class extends Macroable { /** * Route pattern */ #pattern; /** * Matchers inherited from the router */ #globalMatchers; /** * Reference to the AdonisJS application */ #app; /** * Middleware registered on the router */ #routerMiddleware; /** * Reference to route instance. Set after `setHandler` is called */ route = null; constructor(app, routerMiddleware, options) { super(); this.#app = app; this.#routerMiddleware = routerMiddleware; this.#pattern = options.pattern; this.#globalMatchers = options.globalMatchers; } /** * Set handler for the brisk route */ setHandler(handler) { this.route = new Route(this.#app, this.#routerMiddleware, { pattern: this.#pattern, globalMatchers: this.#globalMatchers, methods: ["GET", "HEAD"], handler }); return this.route; } /** * Redirects to a given route. Params from the original request will * be used when no custom params are defined. */ redirect(identifier, params, options) { function redirectsToRoute(ctx) { const redirector = ctx.response.redirect(); if (options?.status) { redirector.status(options.status); } return redirector.toRoute(identifier, params || ctx.params, options); } Object.defineProperty(redirectsToRoute, "listArgs", { value: identifier, writable: false }); return this.setHandler(redirectsToRoute); } /** * Redirect request to a fixed URL */ redirectToPath(url, options) { function redirectsToPath(ctx) { const redirector = ctx.response.redirect(); if (options?.status) { redirector.status(options.status); } return redirector.toPath(url); } Object.defineProperty(redirectsToPath, "listArgs", { value: url, writable: false }); return this.setHandler(redirectsToPath); } }; // src/router/group.ts import Macroable3 from "@poppinss/macroable"; // src/router/resource.ts import string from "@poppinss/utils/string"; import Macroable2 from "@poppinss/macroable"; import { RuntimeException } from "@poppinss/utils"; var RouteResource = class extends Macroable2 { /** * Resource identifier. Nested resources are separated * with a dot notation */ #resource; /** * The controller to handle resource routing requests */ #controller; /** * Is it a shallow resource? Shallow resources URLs do not have parent * resource name and id once they can be identified with the id. */ #shallow = false; /** * Matchers inherited from the router */ #globalMatchers; /** * Reference to the AdonisJS application */ #app; /** * Middleware registered on the router */ #routerMiddleware; /** * Parameter names for the resources. Defaults to `id` for * a singular resource and `resource_id` for nested * resources. */ #params = {}; /** * Base name for the routes. We suffix action names * on top of the base name */ #routesBaseName; /** * A collection of routes instances that belongs to this resource */ routes = []; constructor(app, routerMiddleware, options) { super(); this.#validateResourceName(options.resource); this.#app = app; this.#shallow = options.shallow; this.#routerMiddleware = routerMiddleware; this.#controller = options.controller; this.#globalMatchers = options.globalMatchers; this.#resource = this.#normalizeResourceName(options.resource); this.#routesBaseName = this.#getRoutesBaseName(); this.#buildRoutes(); } /** * Normalizes the resource name to dropping leading and trailing * slashes. */ #normalizeResourceName(resource) { return resource.replace(/^\//, "").replace(/\/$/, ""); } /** * Ensure resource name is not an empty string */ #validateResourceName(resource) { if (!resource || resource === "/") { throw new RuntimeException(`Invalid resource name "${resource}"`); } } /** * Converting segments of a resource to snake case to * make the route name. */ #getRoutesBaseName() { return this.#resource.split(".").map((token) => string.snakeCase(token)).join("."); } /** * Create a new route for the given pattern, methods and controller action */ #createRoute(pattern, methods, action) { const route = new Route(this.#app, this.#routerMiddleware, { pattern, methods, handler: typeof this.#controller === "string" ? `${this.#controller}.${action}` : [this.#controller, action], globalMatchers: this.#globalMatchers }); route.as(`${this.#routesBaseName}.${action}`); this.routes.push(route); } /** * Returns the `resource_id` name for a given resource. The * resource name is converted to singular form and * transformed to snake case. * * photos becomes photo_id * users becomes user_id */ #getResourceId(resource) { return `${string.snakeCase(string.singular(resource))}_id`; } /** * Build routes for the given resource */ #buildRoutes() { const resources = this.#resource.split("."); const mainResource = resources.pop(); this.#params[mainResource] = ":id"; const baseURI = `${resources.map((resource) => { const paramName = `:${this.#getResourceId(resource)}`; this.#params[resource] = paramName; return `${resource}/${paramName}`; }).join("/")}/${mainResource}`; this.#createRoute(baseURI, ["GET", "HEAD"], "index"); this.#createRoute(`${baseURI}/create`, ["GET", "HEAD"], "create"); this.#createRoute(baseURI, ["POST"], "store"); this.#createRoute(`${this.#shallow ? mainResource : baseURI}/:id`, ["GET", "HEAD"], "show"); this.#createRoute(`${this.#shallow ? mainResource : baseURI}/:id/edit`, ["GET", "HEAD"], "edit"); this.#createRoute(`${this.#shallow ? mainResource : baseURI}/:id`, ["PUT", "PATCH"], "update"); this.#createRoute(`${this.#shallow ? mainResource : baseURI}/:id`, ["DELETE"], "destroy"); } /** * Filter the routes based on their partial names */ #filter(names, inverse) { const actions = Array.isArray(names) ? names : [names]; return this.routes.filter((route) => { const match = actions.find((name) => route.getName().endsWith(name)); return inverse ? !match : match; }); } /** * Register only given routes and remove others */ only(names) { this.#filter(names, true).forEach((route) => route.markAsDeleted()); return this; } /** * Register all routes, except the one's defined */ except(names) { this.#filter(names, false).forEach((route) => route.markAsDeleted()); return this; } /** * Register api only routes. The `create` and `edit` routes, which * are meant to show forms will not be registered */ apiOnly() { return this.except(["create", "edit"]); } /** * Define matcher for params inside the resource */ where(key, matcher) { this.routes.forEach((route) => { route.where(key, matcher); }); return this; } tap(actions, callback) { if (typeof actions === "function") { this.routes.forEach((route) => { if (!route.isDeleted()) { actions(route); } }); return this; } this.#filter(actions, false).forEach((route) => { if (!route.isDeleted()) { callback(route); } }); return this; } /** * Set the param name for a given resource */ params(resources) { Object.keys(resources).forEach((resource) => { const param = resources[resource]; const existingParam = this.#params[resource]; this.#params[resource] = `:${param}`; this.routes.forEach((route) => { route.setPattern( route.getPattern().replace(`${resource}/${existingParam}`, `${resource}/:${param}`) ); }); }); return this; } /** * Define one or more middleware on the routes created by * the resource. * * Calling this method multiple times will append middleware * to existing list. */ use(actions, middleware) { if (actions === "*") { this.tap((route) => route.use(middleware)); } else { this.tap(actions, (route) => route.use(middleware)); } return this; } /** * @alias use */ middleware(actions, middleware) { return this.use(actions, middleware); } /** * Prepend name to all the routes */ as(name, normalizeName = true) { name = normalizeName ? string.snakeCase(name) : name; this.routes.forEach((route) => { route.as(route.getName().replace(this.#routesBaseName, name), false); }); this.#routesBaseName = name; return this; } }; // src/router/group.ts var RouteGroup = class _RouteGroup extends Macroable3 { constructor(routes) { super(); this.routes = routes; } /** * Array of middleware registered on the group. */ #middleware = []; /** * Shares midldeware stack with the routes. The method is invoked recursively * to only register middleware with the route class and not with the * resource or the child group */ #shareMiddlewareStackWithRoutes(route) { if (route instanceof _RouteGroup) { route.routes.forEach((child) => this.#shareMiddlewareStackWithRoutes(child)); return; } if (route instanceof RouteResource) { route.routes.forEach((child) => child.getMiddleware().unshift(this.#middleware)); return; } if (route instanceof BriskRoute) { route.route.getMiddleware().unshift(this.#middleware); return; } route.getMiddleware().unshift(this.#middleware); } /** * Updates the route name. The method is invoked recursively to only update * the name with the route class and not with the resource or the child * group. */ #updateRouteName(route, name) { if (route instanceof _RouteGroup) { route.routes.forEach((child) => this.#updateRouteName(child, name)); return; } if (route instanceof RouteResource) { route.routes.forEach((child) => child.as(name, true)); return; } if (route instanceof BriskRoute) { route.route.as(name, true); return; } route.as(name, true); } /** * Sets prefix on the route. The method is invoked recursively to only set * the prefix with the route class and not with the resource or the * child group. */ #setRoutePrefix(route, prefix) { if (route instanceof _RouteGroup) { route.routes.forEach((child) => this.#setRoutePrefix(child, prefix)); return; } if (route instanceof RouteResource) { route.routes.forEach((child) => child.prefix(prefix)); return; } if (route instanceof BriskRoute) { route.route.prefix(prefix); return; } route.prefix(prefix); } /** * Updates domain on the route. The method is invoked recursively to only update * the domain with the route class and not with the resource or the child * group. */ #updateRouteDomain(route, domain) { if (route instanceof _RouteGroup) { route.routes.forEach((child) => this.#updateRouteDomain(child, domain)); return; } if (route instanceof RouteResource) { route.routes.forEach((child) => child.domain(domain)); return; } if (route instanceof BriskRoute) { route.route.domain(domain, false); return; } route.domain(domain, false); } /** * Updates matchers on the route. The method is invoked recursively to only update * the matchers with the route class and not with the resource or the child * group. */ #updateRouteMatchers(route, param, matcher) { if (route instanceof _RouteGroup) { route.routes.forEach((child) => this.#updateRouteMatchers(child, param, matcher)); return; } if (route instanceof RouteResource) { route.routes.forEach((child) => child.where(param, matcher)); return; } if (route instanceof BriskRoute) { route.route.where(param, matcher); return; } route.where(param, matcher); } /** * Define route param matcher * * ```ts * Route.group(() => { * }).where('id', /^[0-9]+/) * ``` */ where(param, matcher) { this.routes.forEach((route) => this.#updateRouteMatchers(route, param, matcher)); return this; } /** * Define prefix all the routes in the group. * * ```ts * Route.group(() => { * }).prefix('v1') * ``` */ prefix(prefix) { this.routes.forEach((route) => this.#setRoutePrefix(route, prefix)); return this; } /** * Define domain for all the routes. * * ```ts * Route.group(() => { * }).domain(':name.adonisjs.com') * ``` */ domain(domain) { this.routes.forEach((route) => this.#updateRouteDomain(route, domain)); return this; } /** * Prepend name to the routes name. * * ```ts * Route.group(() => { * }).as('version1') * ``` */ as(name) { this.routes.forEach((route) => this.#updateRouteName(route, name)); return this; } /** * Prepend an array of middleware to all routes middleware. * * ```ts * Route.group(() => { * }).use(middleware.auth()) * ``` */ use(middleware) { if (!this.#middleware.length) { this.routes.forEach((route) => this.#shareMiddlewareStackWithRoutes(route)); } if (Array.isArray(middleware)) { for (let one of middleware) { this.#middleware.push(one); } } else { this.#middleware.push(middleware); } return this; } /** * @alias use */ middleware(middleware) { return this.use(middleware); } }; // src/helpers.ts var proxyCache = new Cache({ max: 200 }); function dropSlash(input) { if (input === "/") { return "/"; } return `/${input.replace(/^\//, "").replace(/\/$/, "")}`; } function toRoutesJSON(routes) { return routes.reduce((list, route) => { if (route instanceof RouteGroup) { list = list.concat(toRoutesJSON(route.routes)); return list; } if (route instanceof RouteResource) { list = list.concat(toRoutesJSON(route.routes)); return list; } if (route instanceof BriskRoute) { if (route.route && !route.route.isDeleted()) { list.push(route.route.toJSON()); } return list; } if (!route.isDeleted()) { list.push(route.toJSON()); } return list; }, []); } function trustProxy(remoteAddress, proxyFn) { if (proxyCache.has(remoteAddress)) { return proxyCache.get(remoteAddress); } const result = proxyFn(remoteAddress, 0); proxyCache.set(remoteAddress, result); return result; } function parseRange(range, value) { const parts = range.split(".."); const min = Number(parts[0]); const max = Number(parts[1]); if (parts.length === 1 && !Number.isNaN(min)) { return { [min]: value }; } if (Number.isNaN(min) || Number.isNaN(max)) { return {}; } if (min === max) { return { [min]: value }; } if (max < min) { throw new InvalidArgumentsException(`Invalid range "${range}"`); } return [...Array(max - min + 1).keys()].reduce( (result, step) => { result[min + step] = value; return result; }, {} ); } // src/debug.ts import { debuglog } from "node:util"; var debug_default = debuglog("adonisjs:http"); // src/router/route.ts var Route = class extends Macroable4 { /** * Route pattern */ #pattern; /** * HTTP Methods for the route */ #methods; /** * A unique name for the route */ #name; /** * A boolean to prevent route from getting registered within * the store. * * This flag must be set before "Router.commit" method */ #isDeleted = false; /** * Route handler */ #handler; /** * Matchers inherited from the router */ #globalMatchers; /** * Reference to the AdonisJS application */ #app; /** * Middleware registered on the router */ #routerMiddleware; /** * By default the route is part of the `root` domain. Root domain is used * when no domain is defined */ #routeDomain = "root"; /** * An object of matchers to be forwarded to the store. The matchers * list is populated by calling `where` method */ #matchers = {}; /** * Custom prefixes defined on the route or the route parent * groups */ #prefixes = []; /** * Middleware defined directly on the route or the route parent * routes. We mantain an array for each layer of the stack */ #middleware = []; constructor(app, routerMiddleware, options) { super(); this.#app = app; this.#routerMiddleware = routerMiddleware; this.#pattern = options.pattern; this.#methods = options.methods; this.#handler = this.#resolveRouteHandle(options.handler); this.#globalMatchers = options.globalMatchers; } /** * Resolves the route handler string expression to a * handler method object */ #resolveRouteHandle(handler) { if (typeof handler === "string") { const parts = handler.split("."); const method = parts.length === 1 ? "handle" : parts.pop(); const moduleRefId = parts.join("."); return { reference: handler, ...moduleImporter(() => this.#app.import(moduleRefId), method).toHandleMethod(), name: handler }; } if (Array.isArray(handler)) { if (is.class(handler[0])) { return { reference: handler, ...moduleCaller(handler[0], handler[1] || "handle").toHandleMethod() }; } return { reference: handler, ...moduleImporter(handler[0], handler[1] || "handle").toHandleMethod() }; } return handler; } /** * Returns an object of param matchers by merging global and local * matchers. The local copy is given preference over the global * one's */ #getMatchers() { return { ...this.#globalMatchers, ...this.#matchers }; } /** * Returns a normalized pattern string by prefixing the `prefix` (if defined). */ #computePattern() { const pattern = dropSlash(this.#pattern); const prefix = this.#prefixes.slice().reverse().map((one) => dropSlash(one)).join(""); return prefix ? `${prefix}${pattern === "/" ? "" : pattern}` : pattern; } /** * Define matcher for a given param. If a matcher exists, then we do not * override that, since the routes inside a group will set matchers * before the group, so they should have priority over the group * matchers. * * ```ts * Route.group(() => { * Route.get('/:id', 'handler').where('id', /^[0-9]$/) * }).where('id', /[^a-z$]/) * ``` * * The `/^[0-9]$/` will win over the matcher defined by the group */ where(param, matcher) { if (this.#matchers[param]) { return this; } if (typeof matcher === "string") { this.#matchers[param] = { match: new RegExp(matcher) }; } else if (is.regExp(matcher)) { this.#matchers[param] = { match: matcher }; } else { this.#matchers[param] = matcher; } return this; } /** * Define prefix for the route. Calling this method multiple times * applies multiple prefixes in the reverse order. */ prefix(prefix) { this.#prefixes.push(prefix); return this; } /** * Define a custom domain for the route. We do not overwrite the domain * unless `overwrite` flag is set to true. */ domain(domain, overwrite = false) { if (this.#routeDomain === "root" || overwrite) { this.#routeDomain = domain; } return this; } /** * Define one or more middleware to be executed before the route * handler. * * Named middleware can be referenced using the name registered with * the router middleware store. */ use(middleware) { this.#middleware.push(Array.isArray(middleware) ? middleware : [middleware]); return this; } /** * @alias use */ middleware(middleware) { return this.use(middleware); } /** * Give a unique name to the route. Assinging a new unique removes the * existing name of the route. * * Setting prepends to true prefixes the name to the existing name. */ as(name, prepend = false) { if (prepend) { if (!this.#name) { throw new RuntimeException2( `Routes inside a group must have names before calling "router.group.as"` ); } this.#name = `${name}.${this.#name}`; return this; } this.#name = name; return this; } /** * Check if the route was marked to be deleted */ isDeleted() { return this.#isDeleted; } /** * Mark route as deleted. Deleted routes are not registered * with the route store */ markAsDeleted() { this.#isDeleted = true; } /** * Get the route name */ getName() { return this.#name; } /** * Get the route pattern */ getPattern() { return this.#pattern; } /** * Set the route pattern */ setPattern(pattern) { this.#pattern = pattern; return this; } /** * Returns the stack of middleware registered on the route. * The value is shared by reference. */ getMiddleware() { return this.#middleware; } /** * Returns the middleware instance for persistence inside the * store */ #getMiddlewareForStore() { const middleware = new Middleware(); this.#routerMiddleware.forEach((one) => { debug_default("adding global middleware to route %s, %O", this.#pattern, one); middleware.add(one); }); this.#middleware.flat().forEach((one) => { debug_default("adding named middleware to route %s, %O", this.#pattern, one); middleware.add(one); }); return middleware; } /** * Returns JSON representation of the route */ toJSON() { return { domain: this.#routeDomain, pattern: this.#computePattern(), matchers: this.#getMatchers(), meta: {}, name: this.#name, handler: this.#handler, methods: this.#methods, middleware: this.#getMiddlewareForStore(), execute }; } }; // src/cookies/drivers/plain.ts import { base64, MessageBuilder } from "@poppinss/utils"; function pack(value) { if (value === void 0 || value === null) { return null; } return base64.urlEncode(new MessageBuilder().build(value)); } function canUnpack(encodedValue) { return typeof encodedValue === "string"; } function unpack(encodedValue) { return new MessageBuilder().verify(base64.urlDecode(encodedValue, "utf-8", false)); } // src/cookies/drivers/signed.ts function pack2(key, value, encryption) { if (value === void 0 || value === null) { return null; } return `s:${encryption.verifier.sign(value, void 0, key)}`; } function canUnpack2(signedValue) { return typeof signedValue === "string" && signedValue.substring(0, 2) === "s:"; } function unpack2(key, signedValue, encryption) { const value = signedValue.slice(2); if (!value) { return null; } return encryption.verifier.unsign(value, key); } // src/cookies/drivers/encrypted.ts function pack3(key, value, encryption) { if (value === void 0 || value === null) { return null; } return `e:${encryption.encrypt(value, void 0, key)}`; } function canUnpack3(encryptedValue) { return typeof encryptedValue === "string" && encryptedValue.substring(0, 2) === "e:"; } function unpack3(key, encryptedValue, encryption) { const value = encryptedValue.slice(2); if (!value) { return null; } return encryption.decrypt(value, key); } // src/cookies/client.ts var CookieClient = class { #encryption; constructor(encryption) { this.#encryption = encryption; } /** * Encrypt a key value pair to be sent in the cookie header */ encrypt(key, value) { return pack3(key, value, this.#encryption); } /** * Sign a key value pair to be sent in the cookie header */ sign(key, value) { return pack2(key, value, this.#encryption); } /** * Encode a key value pair to be sent in the cookie header */ encode(_, value) { return pack(value); } /** * Unsign a signed cookie value */ unsign(key, value) { return canUnpack2(value) ? unpack2(key, value, this.#encryption) : null; } /** * Decrypt an encrypted cookie value */ decrypt(key, value) { return canUnpack3(value) ? unpack3(key, value, this.#encryption) : null; } /** * Decode an encoded cookie value */ decode(_, value) { return canUnpack(value) ? unpack(value) : null; } /** * Parse response cookie */ parse(key, value) { if (canUnpack2(value)) { return unpack2(key, value, this.#encryption); } if (canUnpack3(value)) { return unpack3(key, value, this.#encryption); } if (canUnpack(value)) { return unpack(value); } } }; // src/request.ts import fresh from "fresh"; import typeIs from "type-is"; import accepts from "accepts"; import { isIP } from "node:net"; import is2 from "@sindresorhus/is"; import proxyaddr from "proxy-addr"; import { safeEqual } from "@poppinss/utils"; import Macroable5 from "@poppinss/macroable"; import lodash from "@poppinss/utils/lodash"; import { createId } from "@paralleldrive/cuid2"; import { parse } from "node:url"; // src/cookies/parser.ts import cookie from "cookie"; var CookieParser = class { #client; /** * A copy of cached cookies, they are cached during a request after * initial decoding, unsigning or decrypting. */ #cachedCookies = { signedCookies: {}, plainCookies: {}, encryptedCookies: {} }; /** * An object of key-value pair collected by parsing * the request cookie header. */ #cookies; constructor(cookieHeader, encryption) { this.#client = new CookieClient(encryption); this.#cookies = this.#parse(cookieHeader); } /** * Parses the request `cookie` header */ #parse(cookieHeader) { if (!cookieHeader) { return {}; } return cookie.parse(cookieHeader); } /** * Attempts to decode a cookie by the name. When calling this method, * you are assuming that the cookie was just encoded in the first * place and not signed or encrypted. */ decode(key, encoded = true) { const value = this.#cookies[key]; if (value === null || value === void 0) { return null; } const cache = this.#cachedCookies.plainCookies; if (cache[key] !== void 0) { return cache[key]; } const parsed = encoded ? this.#client.decode(key, value) : value; if (parsed !== null) { cache[key] = parsed; } return parsed; } /** * Attempts to unsign a cookie by the name. When calling this method, * you are assuming that the cookie was signed in the first place. */ unsign(key) { const value = this.#cookies[key]; if (value === null || value === void 0) { return null; } const cache = this.#cachedCookies.signedCookies; if (cache[key] !== void 0) { return cache[key]; } const parsed = this.#client.unsign(key, value); if (parsed !== null) { cache[key] = parsed; } return parsed; } /** * Attempts to decrypt a cookie by the name. When calling this method, * you are assuming that the cookie was encrypted in the first place. */ decrypt(key) { const value = this.#cookies[key]; if (value === null || value === void 0) { return null; } const cache = this.#cachedCookies.encryptedCookies; if (cache[key] !== void 0) { return cache[key]; } const parsed = this.#client.decrypt(key, value); if (parsed !== null) { cache[key] = parsed; } return parsed; } /** * Returns an object of cookies key-value pair. Do note, the * cookies are not decoded, unsigned or decrypted inside this * list. */ list() { return this.#cookies; } }; // src/request.ts var Request = class extends Macroable5 { constructor(request, response, encryption, config, qsParser) { super(); this.request = request; this.response = response; this.#qsParser = qsParser; this.#config = config; this.#encryption = encryption; this.parsedUrl = parse(this.request.url, false); this.#parseQueryString(); } /** * Query string parser */ #qsParser; /** * Encryption module to verify signed URLs and unsign/decrypt * cookies */ #encryption; /** * Request config */ #config; /** * Request body set using `setBody` method */ #requestBody = {}; /** * A merged copy of `request body` and `querystring` */ #requestData = {}; /** * Original merged copy of `request body` and `querystring`. * Further mutation to this object are not allowed */ #originalRequestData = {}; /** * Parsed query string */ #requestQs = {}; /** * Raw request body as text */ #rawRequestBody; /** * Cached copy of `accepts` fn to do content * negotiation. */ #lazyAccepts; /** * Copy of lazily parsed signed and plain cookies. */ #cookieParser; /** * Parses copy of the URL with query string as a string and not * object. This is done to build URL's with query string without * stringifying the object */ parsedUrl; /** * The ctx will be set by the context itself. It creates a circular * reference */ ctx; /** * Parses the query string */ #parseQueryString() { if (this.parsedUrl.query) { this.updateQs(this.#qsParser.parse(this.parsedUrl.query)); this.#originalRequestData = { ...this.#requestData }; } } /** * Initiates the cookie parser lazily */ #initiateCookieParser() { if (!this.#cookieParser) { this.#cookieParser = new CookieParser(this.header("cookie"), this.#encryption); } } /** * Lazily initiates the `accepts` module to make sure to parse * the request headers only when one of the content-negotiation * methods are used. */ #initiateAccepts() { this.#lazyAccepts = this.#lazyAccepts || accepts(this.request); } /** * Returns the request id from the `x-request-id` header. The * header is untouched, if it already exists. */ id() { let requestId = this.header("x-request-id"); if (!requestId && this.#config.generateRequestId) { requestId = createId(); this.request.headers["x-request-id"] = requestId; } return requestId; } /** * Set initial request body. A copy of the input will be maintained as the original * request body. Since the request body and query string is subject to mutations, we * keep one original reference to flash old data (whenever required). * * This method is supposed to be invoked by the body parser and must be called only * once. For further mutations make use of `updateBody` method. */ setInitialBody(body) { if (this.#originalRequestData && Object.isFrozen(this.#originalRequestData)) { throw new Error('Cannot re-set initial body. Use "request.updateBody" instead'); } this.updateBody(body); this.#originalRequestData = Object.freeze(lodash.cloneDeep(this.#requestData)); } /** * Update the request body with new data object. The `all` property * will be re-computed by merging the query string and request * body. */ updateBody(body) { this.#requestBody = body; this.#requestData = { ...this.#requestBody, ...this.#requestQs }; } /** * Update the request raw body. Bodyparser sets this when unable to parse * the request body or when request is multipart/form-data. */ updateRawBody(rawBody) { this.#rawRequestBody = rawBody; } /** * Update the query string with the new data object. The `all` property * will be re-computed by merging the query and the request body. */ updateQs(data) { this.#requestQs = data; this.#requestData = { ...this.#requestBody, ...this.#requestQs }; } /** * Returns route params */ params() { return this.ctx?.params || {}; } /** * Returns the query string object by reference */ qs() { return this.#requestQs; } /** * Returns reference to the request body */ body() { return this.#requestBody; } /** * Returns reference to the merged copy of request body * and query string */ all() { return this.#requestData; } /** * Returns reference to the merged copy of original request * query string and body */ original() { return this.#originalRequestData; } /** * Returns the request raw body (if exists), or returns `null`. * * Ideally you must be dealing with the parsed body accessed using [[input]], [[all]] or * [[post]] methods. The `raw` body is always a string. */ raw() { return this.#rawRequestBody || null; } /** * Returns value for a given key from the request body or query string. * The `defaultValue` is used when original value is `undefined`. * * @example * ```js * request.input('username') * * // with default value * request.input('username', 'virk') * ``` */ input(key, defaultValue) { return lodash.get(this.#requestData, key, defaultValue); } /** * Returns value for a given key from route params * * @example * ```js * request.param('id') * * // with default value * request.param('id', 1) * ``` */ param(key, defaultValue) { return lodash.get(this.params(), key, defaultValue); } /** * Get everything from the request body except the given keys. * * @example * ```js * request.except(['_csrf']) * ``` */ except(keys) { return lodash.omit(this.#requestData, keys); } /** * Get value for specified keys. * * @example * ```js * request.only(['username', 'age']) * ``` */ only(keys) { return lodash.pick(this.#requestData, keys); } /** * Returns the HTTP request method. This is the original * request method. For spoofed request method, make * use of [[method]]. * * @example * ```js * request.intended() * ``` */ intended() { return this.request.method; } /** * Returns the request HTTP method by taking method spoofing into account. * * Method spoofing works when all of the following are true. * * 1. `app.http.allowMethodSpoofing` config value is true. * 2. request query string has `_method`. * 3. The [[intended]] request method is `POST`. * * @example * ```js * request.method() * ``` */ method() { if (this.#config.allowMethodSpoofing && this.intended() === "POST") { return this.input("_method", this.intended()).toUpperCase(); } return this.intended(); } /** * Returns a copy of headers as an object */ headers() { return this.request.headers; } /** * Returns value for a given header key. The default value is * used when original value is `undefined`. */ header(key, defaultValue) { key = key.toLowerCase(); const headers = this.headers(); switch (key) { case "referer": case "referrer": return headers.referrer || headers.referer || defaultValue; default: return headers[key] || defaultValue; } } /** * Returns the ip address of the user. This method is optimize to fetch * ip address even when running your AdonisJs app behind a proxy. * * You can also define your own custom function to compute the ip address by * defining `app.http.getIp` as a function inside the config file. * * ```js * { * http: { * getIp (request) { * // I am using nginx as a proxy server and want to trust 'x-real-ip' * return request.header('x-real-ip') * } * } * } * ``` * * You can control the behavior of trusting the proxy values by defining it * inside the `config/app.js` file. * * ```js * { * http: { * trustProxy: '127.0.0.1' * } * } * ``` * * The value of trustProxy is passed directly to [proxy-addr](https://www.npmjs.com/package/proxy-addr) */ ip() { const ipFn = this.#config.getIp; if (typeof ipFn === "function") { return ipFn(this); } return proxyaddr(this.request, this.#config.trustProxy); } /** * Returns an array of ip addresses from most to least trusted one. * This method is optimize to fetch ip address even when running * your AdonisJs app behind a proxy. * * You can control the behavior of trusting the proxy values by defining it * inside the `config/app.js` file. * * ```js * { * http: { * trustProxy: '127.0.0.1' * } * } * ``` * * The value of trustProxy is passed directly to [proxy-addr](https://www.npmjs.com/package/proxy-addr) */ ips() { return proxyaddr.all(this.request, this.#config.trustProxy); } /** * Returns the request protocol by checking for the URL protocol or * `X-Forwarded-Proto` header. * * If the `trust` is evaluated to `false`, then URL protocol is returned, * otherwise `X-Forwarded-Proto` header is used (if exists). * * You can control the behavior of trusting the proxy values by defining it * inside the `config/app.js` file. * * ```js * { * http: { * trustProxy: '127.0.0.1' * } * } * ``` * * The value of trustProxy is passed directly to [proxy-addr](https://www.npmjs.com/package/proxy-addr) */ protocol() { if ("encrypted" in this.request.socket) { return "https"; } if (!trustProxy(this.request.socket.remoteAddress, this.#config.trustProxy)) { return this.parsedUrl.protocol || "http"; } const forwardedProtocol = this.header("X-Forwarded-Proto"); return forwardedProtocol ? forwardedProtocol.split(/\s*,\s*/)[0] : "http"; } /** * Returns a boolean telling if request is served over `https` * or not. Check [[protocol]] method to know how protocol is * fetched. */ secure() { return this.protocol() === "https"; } /** * Returns the request host. If proxy headers are trusted, then * `X-Forwarded-Host` is given priority over the `Host` header. * * You can control the behavior of trusting the proxy values by defining it * inside the `config/app.js` file. * * ```js * { * http: { * trustProxy: '127.0.0.1' * } * } * ``` * * The value of trustProxy is passed directly to [proxy-addr](https://www.npmjs.com/package/proxy-addr) */ host() { let host = this.header("host"); if (trustProxy(this.request.socket.remoteAddress, this.#config.trustProxy)) { host = this.header("X-Forwarded-Host") || host; } if (!host) { return null; } return host; } /** * Returns the request hostname. If proxy headers are trusted, then * `X-Forwarded-Host` is given priority over the `Host` header. * * You can control the behavior of trusting the proxy values by defining it * inside the `config/app.js` file. * * ```js * { * http: { * trustProxy: '127.0.0.1' * } * } * ``` * * The value of trustProxy is passed directly to [proxy-addr](https://www.npmjs.com/package/proxy-addr) */ hostname() { const host = this.host(); if (!host) { return null; } const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; const index = host.indexOf(":", offset); return index !== -1 ? host.substring(0, index) : host; } /** * Returns an array of subdomains for the given host. An empty array is * returned if [[hostname]] is `null` or is an IP address. * * Also `www` is not considered as a subdomain */ subdomains() { const hostname = this.hostname(); if (!hostname || isIP(hostname)) { return []; } const offset = this.#config.subdomainOffset; const subdomains = hostname.split(".").reverse().slice(offset); if (subdomains[subdomains.length - 1] === "www") { subdomains.splice(subdomains.length - 1, 1); } return subdomains; } /** * Returns a boolean telling, if request `X-Requested-With === 'xmlhttprequest'` * or not. */ ajax() { const xRequestedWith = this.header("X-Requested-With", ""); return xRequestedWith.toLowerCase() === "xmlhttprequest"; } /** * Returns a boolean telling, if request has `X-Pjax` header * set or not */ pjax() { return !!this.header("X-Pjax"); } /** * Returns the request relative URL. * * @example * ```js * request.url() * * // include query string * request.url(true) * ``` */ url(includeQueryString) { const pathname = this.parsedUrl.pathname; return includeQueryString && this.parsedUrl.query ? `${pathname}?${this.parsedUrl.query}` : pathname; } /** * Returns the complete HTTP url by combining * [[protocol]]://[[hostname]]/[[url]] * * @example * ```js * request.completeUrl() * * // include query string * request.completeUrl(true) * ``` */ completeUrl(includeQueryString) { const protocol = this.protocol(); const hostname = this.host(); return `${protocol}://${hostname}${this.url(includeQueryString)}`; } /** * Find if the current HTTP request is for the given route or the routes */ matchesRoute(routeIdentifier) { if (!this.ctx || !this.ctx.route) { return false; } const route = this.ctx.route; return !!(Array.isArray(routeIdentifier) ? routeIdentifier : [routeIdentifier]).find( (identifier) => { if (route.pattern === identifier || route.name === identifier) { return true; } if (typeof route.handler === "function") { return false; } return route.handler.reference === identifier; } ); } /** * Returns the best matching content type of the request by * matching against the given types. * * The content type is picked from the `content-type` header and request * must have body. * * The method response highly depends upon the types array values. Described below: * * | Type(s) | Return value | * |----------|---------------| * | ['json'] | json | * | ['application/*'] | application/json | * | ['vnd+json'] | application/json | * * @example * ```js * const bodyType = request.is(['json', 'xml']) * * if (bodyType === 'json') { * // process JSON * } * * if (bodyType === 'xml') { * // process XML * } * ``` */ is(types) { return typeIs(this.request, types) || null; } /** * Returns the best type using `Accept` header and * by matching it against the given types. * * If nothing is matched, then `null` will be returned * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. * * @example * ```js * switch (request.accepts(['json', 'html'])) { * case 'json': * return response.json(user) * case 'html': * return view.render('user', { user }) * default: * // decide yourself * } * ``` */ accepts(types) { this.#initiateAccepts(); return this.#lazyAccepts.type(types) || null; } /** * Return the types that the request accepts, in the order of the * client's preference (most preferred first). * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. */ types() { this.#initiateAccepts(); return this.#lazyAccepts.types(); } /** * Returns the best language using `Accept-language` header * and by matching it against the given languages. * * If nothing is matched, then `null` will be returned * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. * * @example * ```js * switch (request.language(['fr', 'de'])) { * case 'fr': * return view.render('about', { lang: 'fr' }) * case 'de': * return view.render('about', { lang: 'de' }) * default: * return view.render('about', { lang: 'en' }) * } * ``` */ language(languages) { this.#initiateAccepts(); return this.#lazyAccepts.language(languages) || null; } /** * Return the languages that the request accepts, in the order of the * client's preference (most preferred first). * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. */ languages() { this.#initiateAccepts(); return this.#lazyAccepts.languages(); } /** * Returns the best charset using `Accept-charset` header * and by matching it against the given charsets. * * If nothing is matched, then `null` will be returned * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. * * @example * ```js * switch (request.charset(['utf-8', 'ISO-8859-1'])) { * case 'utf-8': * // make utf-8 friendly response * case 'ISO-8859-1': * // make ISO-8859-1 friendly response * } * ``` */ charset(charsets) { this.#initiateAccepts(); return this.#lazyAccepts.charset(charsets) || null; } /** * Return the charsets that the request accepts, in the order of the * client's preference (most preferred first). * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. */ charsets() { this.#initiateAccepts(); return this.#lazyAccepts.charsets(); } /** * Returns the best encoding using `Accept-encoding` header * and by matching it against the given encodings. * * If nothing is matched, then `null` will be returned * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. */ encoding(encodings) { this.#initiateAccepts(); return this.#lazyAccepts.encoding(encodings) || null; } /** * Return the charsets that the request accepts, in the order of the * client's preference (most preferred first). * * Make sure to check [accepts](https://www.npmjs.com/package/accepts) package * docs too. */ encodings() { this.#initiateAccepts(); return this.#lazyAccepts.encodings(); } /** * Returns a boolean telling if request has body */ hasBody() { return typeIs.hasBody(this.request); } /** * Returns a boolean telling if the new response etag evaluates same * as the request header `if-none-match`. In case of `true`, the * server must return `304` response, telling the browser to * use the client cache. * * You won't have to deal with this method directly, since AdonisJs will * handle this for you when `http.etag = true` inside `config/app.js` file. * * However, this is how you can use it manually. * * ```js * const responseBody = view.render('some-view') * * // sets the HTTP etag header for response * response.setEtag(responseBody) * * if (request.fresh()) { * response.sendStatus(304) * } else { * response.send(responseBody) * } * ``` */ fresh() { if (["GET", "HEAD"].indexOf(this.intended()) === -1) { return false; } const status = this.response.statusCode; if (status >= 200 && status < 300 || status === 304) { return fresh(this.headers(), this.response.getHeaders()); } return false; } /** * Opposite of [[fresh]] */ stale() { return !this.fresh(); } /** * Returns all parsed and signed cookies. Signed cookies ensures * that their value isn't tampered. */ cookiesList() { this.#initiateCookieParser(); return this.#cookieParser.list(); } /** * Returns value for a given key from signed cookies. Optional * defaultValue is returned when actual value is undefined. */ cookie(key, defaultValue) { this.#initiateCookieParser(); return this.#cookieParser.unsign(key) || defaultValue; } /** * Returns value for a given key from signed cookies. Optional * defaultValue is returned when actual value is undefined. */ encryptedCookie(key, defaultValue) { this.#initiateCookieParser(); return this.#cookieParser.decrypt(key) || defaultValue; } plainCookie(key, defaultValueOrOptions, encoded) { this.#initiateCookieParser(); if (is2.object(defaultValueOrOptions)) { return thi