mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
431 lines
11 KiB
TypeScript
431 lines
11 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-this-alias */
|
|
/* eslint-disable @typescript-eslint/no-namespace */
|
|
import {
|
|
FastifyInstance,
|
|
FastifyPluginCallback,
|
|
FastifyReply,
|
|
FastifyRequest,
|
|
FastifyServerOptions,
|
|
HookHandlerDoneFunction,
|
|
} from 'fastify';
|
|
import fp from 'fastify-plugin';
|
|
import { safeDecodeURI } from 'find-my-way/lib/url-sanitizer';
|
|
import * as http from 'node:http';
|
|
import { Path, pathToRegexp } from 'path-to-regexp';
|
|
import reusify = require('reusify');
|
|
|
|
export type MiddlewareFn<
|
|
Req extends { url: string; originalUrl?: string },
|
|
Res extends { finished?: boolean; writableEnded?: boolean },
|
|
Ctx = unknown,
|
|
> = (req: Req, res: Res, next: (err?: unknown) => void) => void;
|
|
|
|
interface MiddlewareEntry<
|
|
Req extends { url: string; originalUrl?: string },
|
|
Res extends { finished?: boolean; writableEnded?: boolean },
|
|
Ctx,
|
|
> {
|
|
regexp?: RegExp;
|
|
fn: MiddlewareFn<Req, Res, Ctx>;
|
|
}
|
|
|
|
function bindLast<F extends (...args: any[]) => any>(
|
|
fn: F,
|
|
last: Last<Parameters<F>>,
|
|
): (...args: DropLast<Parameters<F>>) => ReturnType<F> {
|
|
return (...args: any[]) => fn(...args, last);
|
|
}
|
|
|
|
// Helper types
|
|
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
|
|
type DropLast<T extends any[]> = T extends [...infer Rest, any] ? Rest : never;
|
|
|
|
/**
|
|
* A clone of `@fastify/middie` engine https://github.com/fastify/middie
|
|
* with an extra vulnerability fix. Path is now decoded before matching to
|
|
* avoid bypassing middleware with encoded characters.
|
|
*/
|
|
function middie<
|
|
Req extends { url: string; originalUrl?: string },
|
|
Res extends { finished?: boolean; writableEnded?: boolean },
|
|
Ctx = unknown,
|
|
>(
|
|
complete: (err: unknown, req: Req, res: Res, ctx: Ctx) => void,
|
|
initialConfig: FastifyServerOptions | null,
|
|
) {
|
|
const middlewares: MiddlewareEntry<Req, Res, Ctx>[] = [];
|
|
const pool = reusify(Holder as any);
|
|
|
|
return {
|
|
use,
|
|
run: bindLast(run, initialConfig),
|
|
};
|
|
|
|
function use(
|
|
this: unknown,
|
|
url:
|
|
| string
|
|
| null
|
|
| MiddlewareFn<Req, Res, Ctx>
|
|
| MiddlewareFn<Req, Res, Ctx>[],
|
|
f?: MiddlewareFn<Req, Res, Ctx> | MiddlewareFn<Req, Res, Ctx>[],
|
|
) {
|
|
if (f === undefined) {
|
|
f = url as MiddlewareFn<Req, Res, Ctx> | MiddlewareFn<Req, Res, Ctx>[];
|
|
url = null;
|
|
}
|
|
|
|
let regexp: RegExp | undefined;
|
|
if (typeof url === 'string') {
|
|
const pathRegExp = pathToRegexp(sanitizePrefixUrl(url) as Path, {
|
|
end: false,
|
|
});
|
|
regexp = pathRegExp.regexp;
|
|
}
|
|
|
|
if (Array.isArray(f)) {
|
|
for (const val of f) {
|
|
middlewares.push({ regexp, fn: val });
|
|
}
|
|
} else {
|
|
middlewares.push({ regexp, fn: f });
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
function run(
|
|
req: Req,
|
|
res: Res,
|
|
ctx: Ctx,
|
|
initialConfig: FastifyServerOptions | null,
|
|
) {
|
|
if (!middlewares.length) {
|
|
complete(null, req, res, ctx);
|
|
return;
|
|
}
|
|
|
|
req.originalUrl = req.url;
|
|
|
|
const holder = pool.get() as any as HolderInstance;
|
|
holder.req = req;
|
|
holder.res = res;
|
|
holder.url = sanitizeUrl(req.url);
|
|
holder.context = ctx;
|
|
holder.initialConfig = initialConfig;
|
|
holder.done();
|
|
}
|
|
|
|
interface HolderInstance {
|
|
req: Req | null;
|
|
res: Res | null;
|
|
url: string | null;
|
|
context: Ctx | null;
|
|
initialConfig: FastifyServerOptions | null;
|
|
i: number;
|
|
done: (err?: unknown) => void;
|
|
}
|
|
|
|
function Holder(this: HolderInstance) {
|
|
this.req = null;
|
|
this.res = null;
|
|
this.url = null;
|
|
this.context = null;
|
|
this.initialConfig = null;
|
|
this.i = 0;
|
|
|
|
const that = this;
|
|
|
|
this.done = function (err?: unknown) {
|
|
const req = that.req!;
|
|
const res = that.res!;
|
|
const url = that.url!;
|
|
const context = that.context!;
|
|
const i = that.i++;
|
|
|
|
req.url = req.originalUrl!;
|
|
|
|
if (res.finished === true || res.writableEnded === true) {
|
|
cleanup();
|
|
return;
|
|
}
|
|
|
|
if (err || middlewares.length === i) {
|
|
complete(err, req, res, context);
|
|
cleanup();
|
|
} else {
|
|
const { fn, regexp } = middlewares[i];
|
|
|
|
if (regexp) {
|
|
// Decode URL before matching to avoid bypassing middleware
|
|
let sanitizedUrl = url;
|
|
if (
|
|
that.initialConfig!.ignoreDuplicateSlashes ||
|
|
that.initialConfig!.routerOptions?.ignoreDuplicateSlashes
|
|
) {
|
|
sanitizedUrl = removeDuplicateSlashes(sanitizedUrl);
|
|
}
|
|
|
|
if (
|
|
that.initialConfig!.ignoreTrailingSlash ||
|
|
that.initialConfig!.routerOptions?.ignoreTrailingSlash
|
|
) {
|
|
sanitizedUrl = trimLastSlash(sanitizedUrl);
|
|
}
|
|
|
|
if (
|
|
that.initialConfig!.caseSensitive === false ||
|
|
that.initialConfig!.routerOptions?.caseSensitive === false
|
|
) {
|
|
sanitizedUrl = sanitizedUrl.toLowerCase();
|
|
}
|
|
|
|
const decodedUrl = safeDecodeURI(
|
|
sanitizedUrl,
|
|
(that.initialConfig?.routerOptions as any)?.useSemicolonDelimiter ||
|
|
that.initialConfig?.useSemicolonDelimiter,
|
|
).path;
|
|
const result = regexp.exec(decodedUrl);
|
|
if (result) {
|
|
req.url = req.url.replace(result[0], '');
|
|
if (req.url[0] !== '/') req.url = '/' + req.url;
|
|
fn(req, res, that.done);
|
|
} else {
|
|
that.done();
|
|
}
|
|
} else {
|
|
fn(req, res, that.done);
|
|
}
|
|
}
|
|
};
|
|
|
|
function cleanup() {
|
|
that.req = null;
|
|
that.res = null;
|
|
that.context = null;
|
|
that.initialConfig = null;
|
|
that.i = 0;
|
|
pool.release(that as any);
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeDuplicateSlashes(path: string) {
|
|
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
|
|
return path.indexOf('//') !== -1
|
|
? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
|
|
: path;
|
|
}
|
|
|
|
function trimLastSlash(path: string) {
|
|
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
|
|
return path.slice(0, -1);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
function sanitizeUrl(url: string): string {
|
|
for (let i = 0, len = url.length; i < len; i++) {
|
|
const charCode = url.charCodeAt(i);
|
|
if (charCode === 63 || charCode === 35) {
|
|
return url.slice(0, i);
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
function sanitizePrefixUrl(url: string): string {
|
|
if (url === '/') return '';
|
|
if (url[url.length - 1] === '/') return url.slice(0, -1);
|
|
return url;
|
|
}
|
|
|
|
const kMiddlewares = Symbol('fastify-middie-middlewares');
|
|
const kMiddie = Symbol('fastify-middie-instance');
|
|
const kMiddieHasMiddlewares = Symbol('fastify-middie-has-middlewares');
|
|
|
|
const supportedHooksWithPayload = [
|
|
'onError',
|
|
'onSend',
|
|
'preParsing',
|
|
'preSerialization',
|
|
] as const;
|
|
|
|
const supportedHooksWithoutPayload = [
|
|
'onRequest',
|
|
'onResponse',
|
|
'onTimeout',
|
|
'preHandler',
|
|
'preValidation',
|
|
] as const;
|
|
|
|
const supportedHooks = [
|
|
...supportedHooksWithPayload,
|
|
...supportedHooksWithoutPayload,
|
|
] as const;
|
|
|
|
type SupportedHook = (typeof supportedHooks)[number];
|
|
|
|
interface MiddieOptions {
|
|
hook?: SupportedHook;
|
|
}
|
|
|
|
function fastifyMiddie(
|
|
fastify: FastifyInstance,
|
|
options: MiddieOptions,
|
|
next: (err?: Error) => void,
|
|
) {
|
|
fastify.decorate('use', use as any);
|
|
fastify[kMiddlewares] = [];
|
|
fastify[kMiddieHasMiddlewares] = false;
|
|
fastify[kMiddie] = middie(onMiddieEnd, fastify.initialConfig);
|
|
|
|
const hook = options.hook || 'onRequest';
|
|
|
|
if (!supportedHooks.includes(hook)) {
|
|
next(new Error(`The hook "${hook}" is not supported by fastify-middie`));
|
|
return;
|
|
}
|
|
|
|
fastify
|
|
.addHook(
|
|
hook,
|
|
supportedHooksWithPayload.includes(hook as any)
|
|
? runMiddieWithPayload
|
|
: runMiddie,
|
|
)
|
|
.addHook('onRegister', onRegister);
|
|
|
|
function use(this: FastifyInstance, path: string | null, fn?: Function) {
|
|
if (typeof path === 'string') {
|
|
const prefix = this.prefix;
|
|
path = prefix + (path === '/' && prefix.length > 0 ? '' : path);
|
|
}
|
|
|
|
this[kMiddlewares].push([path, fn]);
|
|
|
|
if (fn == null) {
|
|
this[kMiddie].use(path);
|
|
} else {
|
|
this[kMiddie].use(path, fn);
|
|
}
|
|
|
|
this[kMiddieHasMiddlewares] = true;
|
|
return this;
|
|
}
|
|
|
|
function runMiddie(
|
|
this: FastifyInstance,
|
|
req: FastifyRequest,
|
|
reply: FastifyReply,
|
|
next: HookHandlerDoneFunction,
|
|
) {
|
|
if (this[kMiddieHasMiddlewares]) {
|
|
const raw = req.raw as any;
|
|
raw.id = req.id;
|
|
raw.hostname = req.hostname;
|
|
raw.protocol = req.protocol;
|
|
raw.ip = req.ip;
|
|
raw.ips = req.ips;
|
|
raw.log = req.log;
|
|
(req.raw as any).query = req.query;
|
|
(reply.raw as any).log = req.log;
|
|
if (req.body !== undefined) (req.raw as any).body = req.body;
|
|
this[kMiddie].run(req.raw, reply.raw, next);
|
|
} else {
|
|
next();
|
|
}
|
|
}
|
|
|
|
function runMiddieWithPayload(
|
|
this: FastifyInstance,
|
|
req: FastifyRequest,
|
|
reply: FastifyReply,
|
|
_payload: unknown,
|
|
next: HookHandlerDoneFunction,
|
|
) {
|
|
runMiddie.bind(this)(req, reply, next);
|
|
}
|
|
|
|
function onMiddieEnd(
|
|
err: unknown,
|
|
_req: any,
|
|
_res: any,
|
|
next: (err?: unknown) => void,
|
|
) {
|
|
next(err);
|
|
}
|
|
|
|
function onRegister(instance: FastifyInstance) {
|
|
const middlewares = instance[kMiddlewares].slice() as Array<Array<unknown>>;
|
|
instance[kMiddlewares] = [];
|
|
instance[kMiddie] = middie(onMiddieEnd, instance.initialConfig);
|
|
instance[kMiddieHasMiddlewares] = false;
|
|
instance.decorate('use', use as any);
|
|
for (const middleware of middlewares) {
|
|
(instance.use as any)(...middleware);
|
|
}
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
/* @eslint-disable-next-line @typescript-eslint/no-namespace */
|
|
declare namespace fastifyMiddie {
|
|
export interface FastifyMiddieOptions {
|
|
hook?:
|
|
| 'onRequest'
|
|
| 'preParsing'
|
|
| 'preValidation'
|
|
| 'preHandler'
|
|
| 'preSerialization'
|
|
| 'onSend'
|
|
| 'onResponse'
|
|
| 'onTimeout'
|
|
| 'onError';
|
|
}
|
|
|
|
type FastifyMiddie =
|
|
FastifyPluginCallback<fastifyMiddie.FastifyMiddieOptions>;
|
|
|
|
export interface IncomingMessageExtended {
|
|
body?: any;
|
|
query?: any;
|
|
}
|
|
export type NextFunction = (err?: any) => void;
|
|
export type SimpleHandleFunction = (
|
|
req: http.IncomingMessage & IncomingMessageExtended,
|
|
res: http.ServerResponse,
|
|
) => void;
|
|
export type NextHandleFunction = (
|
|
req: http.IncomingMessage & IncomingMessageExtended,
|
|
res: http.ServerResponse,
|
|
next: NextFunction,
|
|
) => void;
|
|
|
|
export type Handler = SimpleHandleFunction | NextHandleFunction;
|
|
|
|
export const fastifyMiddie: FastifyMiddie;
|
|
export { fastifyMiddie as default };
|
|
}
|
|
|
|
declare module 'fastify' {
|
|
interface FastifyInstance {
|
|
use(fn: fastifyMiddie.Handler): this;
|
|
use(route: string, fn: fastifyMiddie.Handler): this;
|
|
use(routes: string[], fn: fastifyMiddie.Handler): this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A clone of `@fastify/middie` engine https://github.com/fastify/middie
|
|
* with an extra vulnerability fix. Path is now decoded before matching to
|
|
* avoid bypassing middleware with encoded characters.
|
|
*/
|
|
export default fp(fastifyMiddie, {
|
|
fastify: '5.x',
|
|
name: '@fastify/middie',
|
|
});
|
|
|
|
export { fastifyMiddie };
|