@adonisjs/http-server
Version:
AdonisJS HTTP server with support packed with Routing and Cookies
1,851 lines (1,833 loc) • 127 kB
JavaScript
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