Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions integration/hello-world/e2e/middleware-fastify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,4 +612,158 @@ describe('Middleware (FastifyAdapter)', () => {
await app.close();
});
});

describe('should respect fastify routing options', () => {
const MIDDLEWARE_RETURN_VALUE = 'middleware_return';

@Controller()
class TestController {
@Get('abc/def')
included() {
return 'whatnot';
}
}
@Module({
imports: [AppModule],
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(MIDDLEWARE_RETURN_VALUE))
.forRoutes({ path: 'abc/def', method: RequestMethod.GET });
}
}

describe('[ignoreTrailingSlash] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
ignoreTrailingSlash: true,
// routerOptions: {
// ignoreTrailingSlash: true,
// },
}),
);

await app.init();
});

it(`GET forRoutes(GET /abc/def/)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def/', // trailing slash
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});

afterEach(async () => {
await app.close();
});
});

describe('[ignoreDuplicateSlashes] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
ignoreDuplicateSlashes: true,
},
}),
);

await app.init();
});

it(`GET forRoutes(GET /abc//def)`, () => {
return app
.inject({
method: 'GET',
url: '/abc//def', // duplicate slashes
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});

afterEach(async () => {
await app.close();
});
});

describe('[caseSensitive] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
caseSensitive: true,
},
}),
);

await app.init();
});

it(`GET forRoutes(GET /ABC/DEF)`, () => {
return app
.inject({
method: 'GET',
url: '/ABC/DEF', // different case
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});

afterEach(async () => {
await app.close();
});
});

describe('[useSemicolonDelimiter] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: { useSemicolonDelimiter: true } as any,
}),
);

await app.init();
});

it(`GET forRoutes(GET /abc/def;foo=bar)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def;foo=bar', // semicolon delimiter
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});

afterEach(async () => {
await app.close();
});
});
});
});
49 changes: 48 additions & 1 deletion packages/platform-fastify/adapters/fastify-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,8 @@ export class FastifyAdapter<
queryParamsIndex >= 0
? req.originalUrl.slice(0, queryParamsIndex)
: req.originalUrl;
pathname = safeDecodeURI(pathname).path;

pathname = this.sanitizeUrl(pathname);

if (!re.exec(pathname + '/') && normalizedPath) {
return next();
Expand Down Expand Up @@ -867,4 +868,50 @@ export class FastifyAdapter<
}
return this.instance.route(routeToInject);
}

private sanitizeUrl(url: string): string {
const initialConfig = this.instance.initialConfig as FastifyServerOptions;
const routerOptions =
initialConfig.routerOptions as Partial<FastifyServerOptions>;

if (
routerOptions.ignoreDuplicateSlashes ||
initialConfig.ignoreDuplicateSlashes
) {
url = this.removeDuplicateSlashes(url);
}

if (
routerOptions.ignoreTrailingSlash ||
initialConfig.ignoreTrailingSlash
) {
url = this.trimLastSlash(url);
}

if (
routerOptions.caseSensitive === false ||
initialConfig.caseSensitive === false
) {
url = url.toLowerCase();
}
return safeDecodeURI(
url,
routerOptions.useSemicolonDelimiter ||
initialConfig.useSemicolonDelimiter,
).path;
}

private removeDuplicateSlashes(path: string) {
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
return path.indexOf('//') !== -1
? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
: path;
}

private trimLastSlash(path: string) {
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
return path.slice(0, -1);
}
return path;
}
}
76 changes: 70 additions & 6 deletions packages/platform-fastify/adapters/middie/fastify-middie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FastifyPluginCallback,
FastifyReply,
FastifyRequest,
FastifyServerOptions,
HookHandlerDoneFunction,
} from 'fastify';
import fp from 'fastify-plugin';
Expand All @@ -28,6 +29,17 @@ interface MiddlewareEntry<
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
Expand All @@ -37,13 +49,16 @@ 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) {
>(
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,
run: bindLast(run, initialConfig),
};

function use(
Expand Down Expand Up @@ -79,7 +94,12 @@ function middie<
return this;
}

function run(req: Req, res: Res, ctx: Ctx) {
function run(
req: Req,
res: Res,
ctx: Ctx,
initialConfig: FastifyServerOptions | null,
) {
if (!middlewares.length) {
complete(null, req, res, ctx);
return;
Expand All @@ -92,6 +112,7 @@ function middie<
holder.res = res;
holder.url = sanitizeUrl(req.url);
holder.context = ctx;
holder.initialConfig = initialConfig;
holder.done();
}

Expand All @@ -100,6 +121,7 @@ function middie<
res: Res | null;
url: string | null;
context: Ctx | null;
initialConfig: FastifyServerOptions | null;
i: number;
done: (err?: unknown) => void;
}
Expand All @@ -109,6 +131,7 @@ function middie<
this.res = null;
this.url = null;
this.context = null;
this.initialConfig = null;
this.i = 0;

const that = this;
Expand All @@ -135,7 +158,33 @@ function middie<

if (regexp) {
// Decode URL before matching to avoid bypassing middleware
const decodedUrl = safeDecodeURI(url).path;
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], '');
Expand All @@ -154,12 +203,27 @@ function middie<
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);
Expand Down Expand Up @@ -214,7 +278,7 @@ function fastifyMiddie(
fastify.decorate('use', use as any);
fastify[kMiddlewares] = [];
fastify[kMiddieHasMiddlewares] = false;
fastify[kMiddie] = middie(onMiddieEnd);
fastify[kMiddie] = middie(onMiddieEnd, fastify.initialConfig);

const hook = options.hook || 'onRequest';

Expand Down Expand Up @@ -295,7 +359,7 @@ function fastifyMiddie(
function onRegister(instance: FastifyInstance) {
const middlewares = instance[kMiddlewares].slice() as Array<Array<unknown>>;
instance[kMiddlewares] = [];
instance[kMiddie] = middie(onMiddieEnd);
instance[kMiddie] = middie(onMiddieEnd, instance.initialConfig);
instance[kMiddieHasMiddlewares] = false;
instance.decorate('use', use as any);
for (const middleware of middlewares) {
Expand Down
Loading