/* 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; } function bindLast any>( fn: F, last: Last>, ): (...args: DropLast>) => ReturnType { return (...args: any[]) => fn(...args, last); } // Helper types type Last = T extends [...any[], infer L] ? L : never; type DropLast = 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[] = []; const pool = reusify(Holder as any); return { use, run: bindLast(run, initialConfig), }; function use( this: unknown, url: | string | null | MiddlewareFn | MiddlewareFn[], f?: MiddlewareFn | MiddlewareFn[], ) { if (f === undefined) { f = url as MiddlewareFn | MiddlewareFn[]; 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>; 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; 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 };