Skip to content

Commit

Permalink
fix(apps/legacy-api): reject with 404 for queries with invalid networ…
Browse files Browse the repository at this point in the history
…k url param (#1089)

* fix(apps/legacy-api): protect against invalid networks

- refactor 'network' param to use NetworkName type def
- add test guarding all endpoints

* fix(apps/legacy-api): remove regtest from supported networks

* chore(apps/legacy-api): change inline doc to jsdoc format

* chore(apps/legacy-api): move supported networks into NetworkValidationPipe
  • Loading branch information
eli-lim authored Feb 24, 2022
1 parent bba042d commit 383d355
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { LegacyApiTesting } from '../../testing/LegacyApiTesting'

const apiTesting = LegacyApiTesting.create()

describe('NetworkParamValidation', () => {
beforeAll(async () => {
await apiTesting.start()
})

afterAll(async () => {
await apiTesting.stop()
})

it('all registered routes are guarded against invalid network param', async () => {
// dummy call to trigger fastify hooks so that all routes are registered
await apiTesting.app.inject({
method: 'GET',
url: '/v1'
}).catch()

const routes = apiTesting.getAllRoutes()
for (const { url } of routes) {
if (url === '*') {
continue
}

const result = await apiTesting.app.inject({
method: 'GET',
url: url + '?network=abc' // Query with some invalid network
})

expect(result.json()).toStrictEqual({
message: 'Not Found',
statusCode: 404
})
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,12 @@ describe('WhaleApiClientProvider', () => {
expect(first === second) // points to the same object
.toStrictEqual(true)
}
{
const first = whaleApiClientProvider.getClient('regtest')
const second = whaleApiClientProvider.getClient('regtest')
expect(first === second) // points to the same object
.toStrictEqual(true)
}
})

it('should return different clients for different networks', () => {
const mainnet = whaleApiClientProvider.getClient('mainnet')
const testnet = whaleApiClientProvider.getClient('testnet')
const regtest = whaleApiClientProvider.getClient('regtest')

expect(mainnet).not.toStrictEqual(testnet)
expect(mainnet).not.toStrictEqual(regtest)
expect(testnet).not.toStrictEqual(regtest)
})
})
3 changes: 2 additions & 1 deletion apps/legacy-api/src/controllers/MiscController.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Controller, Get, Query } from '@nestjs/common'
import { StatsData } from '@defichain/whale-api-client/dist/api/stats'
import { WhaleApiClientProvider } from '../providers/WhaleApiClientProvider'
import { NetworkValidationPipe, SupportedNetwork } from '../pipes/NetworkValidationPipe'

@Controller('v1')
export class MiscController {
constructor (private readonly whaleApiClientProvider: WhaleApiClientProvider) {}

@Get('getblockcount')
async getToken (
@Query('network') network: 'mainnet' | 'testnet' | 'regtest' = 'mainnet'
@Query('network', NetworkValidationPipe) network: SupportedNetwork = 'mainnet'
): Promise<{ [key: string]: Number }> {
const api = this.whaleApiClientProvider.getClient(network)

Expand Down
5 changes: 3 additions & 2 deletions apps/legacy-api/src/controllers/PoolPairController.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Controller, Get, Query } from '@nestjs/common'
import { PoolPairData } from '@defichain/whale-api-client/dist/api/poolpairs'
import { WhaleApiClientProvider } from '../providers/WhaleApiClientProvider'
import { NetworkValidationPipe, SupportedNetwork } from '../pipes/NetworkValidationPipe'

@Controller('v1')
export class PoolPairController {
constructor (private readonly whaleApiClientProvider: WhaleApiClientProvider) {}

@Get('getpoolpair')
async getToken (
@Query('network') network: 'mainnet' | 'testnet' | 'regtest' = 'mainnet',
@Query('network', NetworkValidationPipe) network: SupportedNetwork = 'mainnet',
@Query('id') poolPairId: string
): Promise<LegacyPoolPairData> {
const api = this.whaleApiClientProvider.getClient(network)
Expand All @@ -18,7 +19,7 @@ export class PoolPairController {

@Get('listpoolpairs')
async listPoolPairs (
@Query('network') network: 'mainnet' | 'testnet' | 'regtest' = 'mainnet'
@Query('network', NetworkValidationPipe) network: SupportedNetwork = 'mainnet'
): Promise<{ [key: string]: LegacyPoolPairData }> {
const api = this.whaleApiClientProvider.getClient(network)

Expand Down
5 changes: 3 additions & 2 deletions apps/legacy-api/src/controllers/TokenController.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Controller, Get, Query } from '@nestjs/common'
import { TokenData } from '@defichain/whale-api-client/dist/api/tokens'
import { WhaleApiClientProvider } from '../providers/WhaleApiClientProvider'
import { NetworkValidationPipe, SupportedNetwork } from '../pipes/NetworkValidationPipe'

@Controller('v1')
export class TokenController {
constructor (private readonly whaleApiClientProvider: WhaleApiClientProvider) {}

@Get('gettoken')
async getToken (
@Query('network') network: 'mainnet' | 'testnet' | 'regtest' = 'mainnet',
@Query('network', NetworkValidationPipe) network: SupportedNetwork = 'mainnet',
@Query('id') tokenId: string
): Promise<{ [key: string]: LegacyTokenData }> {
const api = this.whaleApiClientProvider.getClient(network)
Expand All @@ -21,7 +22,7 @@ export class TokenController {

@Get('listtokens')
async listTokens (
@Query('network') network: 'mainnet' | 'testnet' | 'regtest' = 'mainnet',
@Query('network', NetworkValidationPipe) network: SupportedNetwork = 'mainnet',
@Query('id') tokenId: string
): Promise<{ [key: string]: LegacyTokenData }> {
const api = this.whaleApiClientProvider.getClient(network)
Expand Down
33 changes: 33 additions & 0 deletions apps/legacy-api/src/pipes/NetworkValidationPipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
ArgumentMetadata,
HttpException,
Injectable,
PipeTransform
} from '@nestjs/common'

export type SupportedNetwork = 'mainnet' | 'testnet'

@Injectable()
export class NetworkValidationPipe implements PipeTransform {
private static readonly VALID_NETWORKS: Set<undefined | SupportedNetwork> = new Set([
undefined, // defaults to 'mainnet'
'mainnet',
'testnet'
])

transform (value: any, metadata: ArgumentMetadata): any {
if (NetworkValidationPipe.VALID_NETWORKS.has(value)) {
return value
}
throw new InvalidNetworkException()
}
}

export class InvalidNetworkException extends HttpException {
constructor () {
super({
statusCode: 404,
message: 'Not Found'
}, 404)
}
}
9 changes: 4 additions & 5 deletions apps/legacy-api/src/providers/WhaleApiClientProvider.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { WhaleApiClient } from '@defichain/whale-api-client'
import { Injectable } from '@nestjs/common'

type Network = 'mainnet' | 'testnet' | 'regtest'
import { SupportedNetwork } from '../pipes/NetworkValidationPipe'

@Injectable()
export class WhaleApiClientProvider {
private readonly clientCacheByNetwork: Map<Network, WhaleApiClient> = new Map()
private readonly clientCacheByNetwork: Map<SupportedNetwork, WhaleApiClient> = new Map()

/**
* Lazily initialises WhaleApiClients and caches them by network for performance.
* @param network - the network to connect to
*/
getClient (network: Network): WhaleApiClient {
getClient (network: SupportedNetwork): WhaleApiClient {
const client = this.clientCacheByNetwork.get(network)
if (client !== undefined) {
return client
}
return this.createAndCacheClient(network)
}

private createAndCacheClient (network: Network): WhaleApiClient {
private createAndCacheClient (network: SupportedNetwork): WhaleApiClient {
const client = new WhaleApiClient({
version: 'v0',
network: network,
Expand Down
6 changes: 5 additions & 1 deletion apps/legacy-api/testing/LegacyApiTesting.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LegacyStubServer } from './LegacyStubServer'
import { LegacyStubServer, RegisteredRoute } from './LegacyStubServer'
import { NestFastifyApplication } from '@nestjs/platform-fastify'
import { InjectOptions, Response as LightMyRequestResponse } from 'light-my-request'

Expand Down Expand Up @@ -51,4 +51,8 @@ export class LegacyApiTesting {
console.error(err)
}
}

getAllRoutes (): RegisteredRoute[] {
return this.stubServer.getAllRoutes()
}
}
32 changes: 31 additions & 1 deletion apps/legacy-api/testing/LegacyStubServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config'
* Service stubs are simulations of a real service, which are used for functional testing.
*/
export class LegacyStubServer extends RootServer {
private readonly allRoutes: RegisteredRoute[] = []

async create (): Promise<NestFastifyApplication> {
const module = await Test.createTestingModule({
imports: [
Expand All @@ -21,10 +23,38 @@ export class LegacyStubServer extends RootServer {
const adapter = new FastifyAdapter({
logger: false
})
return module.createNestApplication<NestFastifyApplication>(adapter)
const app = module.createNestApplication<NestFastifyApplication>(adapter)
this.recordAllRegisteredRoutes(app)
return app
}

async init (app: NestFastifyApplication, config: ConfigService): Promise<void> {
await app.init()
}

/**
* Helper to get all the registered routes for testing purposes
*/
getAllRoutes (): RegisteredRoute[] {
return this.allRoutes
}

private recordAllRegisteredRoutes (app: NestFastifyApplication): void {
app.getHttpAdapter()
.getInstance()
.addHook('onRoute', (opts: RegisteredRoute) => {
this.allRoutes.push(opts)
})
}
}

/**
* @see https://www.fastify.io/docs/latest/Reference/Hooks/#onroute
*/
export interface RegisteredRoute {
method: string
url: string // the complete URL of the route, it will include the prefix if any
path: string // `url` alias
routePath: string // the URL of the route without the prefix
prefix: string
}

0 comments on commit 383d355

Please sign in to comment.