diff --git a/config/config.devnet-old.yaml b/config/config.devnet-old.yaml index 8f61aac89..3cfa0a2d9 100644 --- a/config/config.devnet-old.yaml +++ b/config/config.devnet-old.yaml @@ -129,3 +129,8 @@ inflation: nftProcess: parallelism: 1 maxRetries: 3 +restrictedRoutes: + enabled: true + routes: + - '/package.json' + - '/docs/package.json' diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 8d76292b7..71c7dd14c 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -204,3 +204,8 @@ compression: level: 6 threshold: 1024 chunkSize: 16384 +restrictedRoutes: + enabled: true + routes: + - '/package.json' + - '/docs/package.json' diff --git a/config/config.e2e-mocked.mainnet.yaml b/config/config.e2e-mocked.mainnet.yaml index f65f05065..4cf36ee31 100644 --- a/config/config.e2e-mocked.mainnet.yaml +++ b/config/config.e2e-mocked.mainnet.yaml @@ -86,3 +86,8 @@ test: transaction-action: mex: microServiceUrl: 'https://graph.xexchange.com/graphql' +restrictedRoutes: + enabled: true + routes: + - '/package.json' + - '/docs/package.json' diff --git a/config/config.e2e.mainnet.yaml b/config/config.e2e.mainnet.yaml index 50da09bff..b37db057a 100644 --- a/config/config.e2e.mainnet.yaml +++ b/config/config.e2e.mainnet.yaml @@ -197,4 +197,9 @@ stakingV5Inflation: - 1262802 nftProcess: parallelism: 1 - maxRetries: 3 \ No newline at end of file + maxRetries: 3 +restrictedRoutes: + enabled: true + routes: + - '/package.json' + - '/docs/package.json' diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index a90823e54..ffa6a3134 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -212,3 +212,9 @@ customUrlHeaders: - urlPattern: '' headers: x-custom-auth: '' +restrictedRoutes: + enabled: true + routes: + - '/package.json' + - '/docs/package.json' + diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 802f4b574..a1966bc3e 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -207,3 +207,8 @@ compression: level: 6 threshold: 1024 chunkSize: 16384 +restrictedRoutes: + enabled: true + routes: + - '/package.json' + - '/docs/package.json' diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index f84ad3aa0..9a4e376d9 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -1104,4 +1104,12 @@ export class ApiConfigService { return timestamp; } + + isRestrictedRoutesEnabled(): boolean { + return this.configService.get('restrictedRoutes.enabled') ?? false; + } + + getRestrictedRoutes(): string[] { + return this.configService.get('restrictedRoutes.routes') ?? []; + } } diff --git a/src/main.ts b/src/main.ts index aca22383e..339a60fc2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,6 +38,7 @@ import * as requestIp from 'request-ip'; import compression from 'compression'; import { IoAdapter } from '@nestjs/platform-socket.io'; import { WebsocketSubscriptionModule } from './crons/websocket/websocket.subscription.module'; +import { RestrictedRoutesMiddleware } from './utils/restricted.routes.middleware'; async function bootstrap() { const logger = new Logger('Bootstrapper'); @@ -186,9 +187,16 @@ async function bootstrap() { logger.log(`Guest caching enabled: ${apiConfigService.isGuestCacheFeatureActive()}`); logger.log(`Transaction pool enabled: ${apiConfigService.isTransactionPoolEnabled()}`); logger.log(`Transaction pool cache warmer enabled: ${apiConfigService.isTransactionPoolCacheWarmerEnabled()}`); + + logger.log(`Restricted routes enabled: ${apiConfigService.isRestrictedRoutesEnabled()}`); } async function configurePublicApp(publicApp: NestExpressApplication, apiConfigService: ApiConfigService) { + if (apiConfigService.isRestrictedRoutesEnabled()) { + const restrictedRoutesMiddleware = publicApp.get(RestrictedRoutesMiddleware); + publicApp.use(restrictedRoutesMiddleware.use.bind(restrictedRoutesMiddleware)); + } + if (apiConfigService.getCompressionEnabled()) { publicApp.use(compression({ filter: (req: any, res: any) => { diff --git a/src/public.app.module.ts b/src/public.app.module.ts index 426717e6d..5c48e6f81 100644 --- a/src/public.app.module.ts +++ b/src/public.app.module.ts @@ -9,6 +9,7 @@ import { GuestCacheService } from '@multiversx/sdk-nestjs-cache'; import { LoggingModule } from '@multiversx/sdk-nestjs-common'; import { DynamicModuleUtils } from './utils/dynamic.module.utils'; import { LocalCacheController } from './endpoints/caching/local.cache.controller'; +import { RestrictedRoutesMiddleware } from './utils/restricted.routes.middleware'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { LocalCacheController } from './endpoints/caching/local.cache.controller providers: [ DynamicModuleUtils.getNestJsApiConfigService(), GuestCacheService, + RestrictedRoutesMiddleware, ], exports: [ EndpointsServicesModule, diff --git a/src/test/unit/utils/restricted.routes.middleware.spec.ts b/src/test/unit/utils/restricted.routes.middleware.spec.ts new file mode 100644 index 000000000..4fdca0b5b --- /dev/null +++ b/src/test/unit/utils/restricted.routes.middleware.spec.ts @@ -0,0 +1,45 @@ +import { NotFoundException } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { ApiConfigService } from 'src/common/api-config/api.config.service'; +import { RestrictedRoutesMiddleware } from 'src/utils/restricted.routes.middleware'; + +describe('RestrictedRoutesMiddleware', () => { + let middleware: RestrictedRoutesMiddleware; + let apiConfigService: jest.Mocked; + + beforeEach(() => { + apiConfigService = { + getRestrictedRoutes: jest.fn(), + } as unknown as jest.Mocked; + + middleware = new RestrictedRoutesMiddleware(apiConfigService); + }); + + it('should throw NotFoundException when route is restricted', () => { + apiConfigService.getRestrictedRoutes.mockReturnValue(['/blocked']); + + const req = { + path: '/blocked', + } as Request; + const res = {} as Response; + const next = jest.fn(); + + expect(() => middleware.use(req, res, next)).toThrow(NotFoundException); + expect(() => middleware.use(req, res, next)).toThrow('Cannot GET /blocked'); + expect(next).not.toHaveBeenCalled(); + }); + + it('should call next when route is not restricted', () => { + apiConfigService.getRestrictedRoutes.mockReturnValue(['/blocked']); + + const req = { + path: '/allowed', + } as Request; + const res = {} as Response; + const next = jest.fn(); + + middleware.use(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/utils/restricted.routes.middleware.ts b/src/utils/restricted.routes.middleware.ts new file mode 100644 index 000000000..e8f92cb44 --- /dev/null +++ b/src/utils/restricted.routes.middleware.ts @@ -0,0 +1,19 @@ +import { Injectable, NestMiddleware, NotFoundException } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import { ApiConfigService } from 'src/common/api-config/api.config.service'; + +@Injectable() +export class RestrictedRoutesMiddleware implements NestMiddleware { + constructor( + private readonly apiConfigService: ApiConfigService, + ) { } + + use(req: Request, _res: Response, next: NextFunction) { + const restrictedRoutes = this.apiConfigService.getRestrictedRoutes(); + if (restrictedRoutes.includes(req.path)) { + throw new NotFoundException(`Cannot GET ${req.path}`); + } + + next(); + } +}