Skip to content
This repository has been archived by the owner on Jul 4, 2023. It is now read-only.

Commit

Permalink
fix(serve): validating incoming headers in serve mode
Browse files Browse the repository at this point in the history
Previously they were not being checked

fix #4
  • Loading branch information
tamj0rd2 authored and probot-auto-merge[bot] committed May 16, 2020
1 parent ee43925 commit bcfc762
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 15 deletions.
8 changes: 4 additions & 4 deletions src/commands/serve/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface ValidatedServeConfig {
request: {
method: SupportedMethod
type?: string
headers?: StringDict
headers?: NcdcHeaders
endpoints?: string[]
serveEndpoint?: string
body?: Data
Expand All @@ -16,7 +16,7 @@ export interface ValidatedServeConfig {
response: {
code: number
type?: string
headers?: StringDict
headers?: NcdcHeaders
body?: Data
bodyPath?: string
serveBody?: Data
Expand Down Expand Up @@ -81,12 +81,12 @@ export interface ServeConfig {
endpoint: string
body?: Data
type?: string
headers?: StringDict
headers?: NcdcHeaders
}
response: {
code: number
body?: Data
type?: string
headers?: StringDict
headers?: NcdcHeaders
}
}
55 changes: 55 additions & 0 deletions src/commands/serve/server/header-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { areHeadersValid, HeaderValidationResult } from './header-validator'

jest.disableAutomock()

describe('areHeadersValid', () => {
it('returns true if the headers match the config', () => {
const expected = {
'x-hello': 'world',
'what-is': 'love',
baby: 'dont,hurt,me',
}

const result = areHeadersValid(expected, {
'X-Hello': 'world',
'what-is': 'love',
baby: ['dont', 'hurt', 'me'],
})

expect(result).toMatchObject<HeaderValidationResult>({ success: true })
})

it('returns false if headers are missing', () => {
const expected = {
hello: 'world',
'what-is': 'love',
baby: 'dont,hurt,me',
}

const result = areHeadersValid(expected, {
Hello: 'world',
'what-is': 'Love',
baby: ['dont', 'hurt', 'me'],
})

expect(result).toMatchObject<HeaderValidationResult>({
success: false,
})
})

it('returns false if header values do not match case', () => {
const expected = {
hello: 'world',
'what-is': 'love',
baby: 'dont,hurt,me',
}

const result = areHeadersValid(expected, {
hello: 'world',
'what-is': 'love',
baby: ['dont', 'Hurt', 'me'],
})

expect(result).toMatchObject<HeaderValidationResult>({ success: false })
})
})
46 changes: 46 additions & 0 deletions src/commands/serve/server/header-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { IncomingHttpHeaders } from 'http2'

export type HeaderValidationResult =
| {
success: true
}
| {
success: false
}

export const areHeadersValid = (
origExpectedHeaders: NcdcHeaders,
origReceivedHeaders: IncomingHttpHeaders,
): HeaderValidationResult => {
const expectedHeaders: NcdcHeaders = {}
for (const key in origExpectedHeaders) {
expectedHeaders[key.toLowerCase()] = origExpectedHeaders[key]
}

const receivedHeaders: IncomingHttpHeaders = {}
for (const key in origReceivedHeaders) {
receivedHeaders[key.toLowerCase()] = origReceivedHeaders[key]
}

for (const key in expectedHeaders) {
const expected = expectedHeaders[key]
const actual = receivedHeaders[key]
const badResult = { success: false }

if (expected.includes(',')) {
if (!Array.isArray(actual)) return badResult
for (const item of expected.split(',')) {
if (!actual.includes(item)) return badResult
}
break
}

if (Array.isArray(receivedHeaders)) {
if (!actual?.includes(expected)) return badResult
} else {
if (actual !== expected) return badResult
}
}

return { success: true }
}
9 changes: 9 additions & 0 deletions src/commands/serve/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ServeConfig } from '../config'
import validateQuery from './query-validator'
import { SupportedMethod } from '~config/types'
import { logMetric } from '~metrics'
import { areHeadersValid } from './header-validator'

export interface ReqResLog {
name?: string
Expand Down Expand Up @@ -81,6 +82,14 @@ export const configureServer = (

app[verbsMap[request.method]](endpointWithoutQuery, async (req, res, next) => {
try {
if (request.headers) {
const { success } = areHeadersValid(request.headers, req.headers)
if (!success) {
logger.warn(`An endpoint for ${req.path} exists but the headers did not match the configuration`)
return next()
}
}

const queryIsValid = validateQuery(request.endpoint, req.query)
if (!queryIsValid) {
logger.warn(
Expand Down
20 changes: 18 additions & 2 deletions src/commands/serve/server/server.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ describe('server', () => {
})

describe('request', () => {
describe('headers', () => {
const config = new ConfigBuilder().withResponseCode(200).withRequestHeaders({ nice: 'meme' }).build()

it('fails when configured headers are not given', async () => {
const app = getApp([config])

await request(app).get(config.request.endpoint).send().expect(404)
})

it('passes when configured headers are given', async () => {
const app = getApp([config])

await request(app).get(config.request.endpoint).set('nice', 'meme').expect(200)
})
})

describe('query', () => {
it('still responds when a query matches in a different order', async () => {
const configs = [
Expand Down Expand Up @@ -155,15 +171,15 @@ describe('server', () => {
.withMethod('POST')
.withEndpoint('/config1')
.withRequestType('number')
.withResponseCode(404)
.withResponseCode(401)
.withResponseBody('Noice')
.build(),
]
mockTypeValidator.validate.mockResolvedValue({ success: true })

const app = getApp(configs)

await request(app).post(configs[0].request.endpoint).send('Yo dude!').expect(404).expect('Noice')
await request(app).post(configs[0].request.endpoint).send('Yo dude!').expect(401).expect('Noice')
})

it('gives a 404 when the request body fails type validation', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/test/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ export interface ValidatedTestConfig {
request: {
method: SupportedMethod
type?: string
headers?: StringDict
headers?: NcdcHeaders
endpoints: string[]
body?: Data
bodyPath?: string
}
response: {
code: number
type?: string
headers?: StringDict
headers?: NcdcHeaders
body?: Data
bodyPath?: string
}
Expand Down
8 changes: 4 additions & 4 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ export interface CommonConfig {
endpoint: string
body?: Data
type?: string
headers?: StringDict
headers?: NcdcHeaders
}
response: {
code: number
body?: Data
type?: string
headers?: StringDict
headers?: NcdcHeaders
}
}

Expand Down Expand Up @@ -50,7 +50,7 @@ export class ConfigBuilder {
return this
}

public withRequestHeaders(headers: Optional<StringDict>): ConfigBuilder {
public withRequestHeaders(headers: Optional<NcdcHeaders>): ConfigBuilder {
this.config.request.headers = headers
return this
}
Expand All @@ -70,7 +70,7 @@ export class ConfigBuilder {
return this
}

public withResponseHeaders(headers: Optional<StringDict>): ConfigBuilder {
public withResponseHeaders(headers: Optional<NcdcHeaders>): ConfigBuilder {
this.config.response.headers = headers
return this
}
Expand Down
5 changes: 3 additions & 2 deletions src/config/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const validateRawConfig = <TOut = ValidatedRawConfig>(
})

const bodySchema = [Joi.string(), Joi.object()]
const headersSchema = Joi.object().pattern(Joi.string(), Joi.string())

const schema = Joi.array()
.items(
Expand All @@ -62,7 +63,7 @@ export const validateRawConfig = <TOut = ValidatedRawConfig>(
.uppercase()
.required(),
type: Joi.string(),
headers: Joi.object(),
headers: headersSchema,
endpoints: Joi.alternatives()
.conditional('...serveOnly', {
is: Joi.valid(false),
Expand All @@ -83,7 +84,7 @@ export const validateRawConfig = <TOut = ValidatedRawConfig>(
response: Joi.object({
code: Joi.number().required(),
type: Joi.string(),
headers: Joi.object(),
headers: headersSchema,
body: bodySchema,
bodyPath: Joi.string(),
serveBody: bodySchema,
Expand Down
2 changes: 1 addition & 1 deletion src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ declare type ROPopulatedArray<T> = { 0: T } & ReadonlyArray<T>
type DataItem = string | number | boolean | null | DataObject | DataItem[]
type DataObject = { [index: string]: DataItem }
declare type Data = DataObject | DataObject[] | string
declare type StringDict = Record<string, string>
declare type NcdcHeaders = Record<string, string>

0 comments on commit bcfc762

Please sign in to comment.