-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7d303eb
commit 7fdc347
Showing
23 changed files
with
643 additions
and
476 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# @kbn/utils | ||
|
||
Shared server-side utilities shared across packages and plugins. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
module.exports = { | ||
preset: '@kbn/test', | ||
rootDir: '../..', | ||
roots: ['<rootDir>/packages/kbn-http-tools'], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "@kbn/http-tools", | ||
"main": "./target/index.js", | ||
"version": "1.0.0", | ||
"license": "SSPL-1.0 OR Elastic License 2.0", | ||
"private": true, | ||
"scripts": { | ||
"build": "rm -rf target && ../../node_modules/.bin/tsc", | ||
"kbn:bootstrap": "yarn build", | ||
"kbn:watch": "yarn build --watch" | ||
}, | ||
"dependencies": { | ||
"@kbn/config-schema": "link:../kbn-config-schema", | ||
"@kbn/std": "link:../kbn-std" | ||
}, | ||
"devDependencies": { | ||
"@kbn/utility-types": "link:../kbn-utility-types" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { Server, ServerOptions } from '@hapi/hapi'; | ||
import { ListenerOptions } from './get_listener_options'; | ||
|
||
export function createServer(serverOptions: ServerOptions, listenerOptions: ListenerOptions) { | ||
const server = new Server(serverOptions); | ||
|
||
server.listener.keepAliveTimeout = listenerOptions.keepaliveTimeout; | ||
server.listener.setTimeout(listenerOptions.socketTimeout); | ||
server.listener.on('timeout', (socket) => { | ||
socket.destroy(); | ||
}); | ||
server.listener.on('clientError', (err, socket) => { | ||
if (socket.writable) { | ||
socket.end(Buffer.from('HTTP/1.1 400 Bad Request\r\n\r\n', 'ascii')); | ||
} else { | ||
socket.destroy(err); | ||
} | ||
}); | ||
|
||
return server; | ||
} |
51 changes: 51 additions & 0 deletions
51
packages/kbn-http-tools/src/default_validation_error_handler.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import Joi from 'joi'; | ||
import { Request, ResponseToolkit } from '@hapi/hapi'; | ||
import { | ||
defaultValidationErrorHandler, | ||
HapiValidationError, | ||
} from './default_validation_error_handler'; | ||
|
||
const emptyOutput = { | ||
statusCode: 400, | ||
headers: {}, | ||
payload: { | ||
statusCode: 400, | ||
error: '', | ||
validation: { | ||
source: '', | ||
keys: [], | ||
}, | ||
}, | ||
}; | ||
|
||
describe('defaultValidationErrorHandler', () => { | ||
it('formats value validation errors correctly', () => { | ||
expect.assertions(1); | ||
const schema = Joi.array().items( | ||
Joi.object({ | ||
type: Joi.string().required(), | ||
}).required() | ||
); | ||
|
||
const error = schema.validate([{}], { abortEarly: false }).error as HapiValidationError; | ||
|
||
// Emulate what Hapi v17 does by default | ||
error.output = { ...emptyOutput }; | ||
error.output.payload.validation.keys = ['0.type', '']; | ||
|
||
try { | ||
defaultValidationErrorHandler({} as Request, {} as ResponseToolkit, error); | ||
} catch (err) { | ||
// Verify the empty string gets corrected to 'value' | ||
expect(err.output.payload.validation.keys).toEqual(['0.type', 'value']); | ||
} | ||
}); | ||
}); |
63 changes: 63 additions & 0 deletions
63
packages/kbn-http-tools/src/default_validation_error_handler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { Lifecycle, Request, ResponseToolkit, Util } from '@hapi/hapi'; | ||
import { ValidationError } from 'joi'; | ||
import Hoek from '@hapi/hoek'; | ||
|
||
/** | ||
* Hapi extends the ValidationError interface to add this output key with more data. | ||
*/ | ||
export interface HapiValidationError extends ValidationError { | ||
output: { | ||
statusCode: number; | ||
headers: Util.Dictionary<string | string[]>; | ||
payload: { | ||
statusCode: number; | ||
error: string; | ||
message?: string; | ||
validation: { | ||
source: string; | ||
keys: string[]; | ||
}; | ||
}; | ||
}; | ||
} | ||
|
||
/** | ||
* Used to replicate Hapi v16 and below's validation responses. Should be used in the routes.validate.failAction key. | ||
*/ | ||
export function defaultValidationErrorHandler( | ||
request: Request, | ||
h: ResponseToolkit, | ||
err?: Error | ||
): Lifecycle.ReturnValue { | ||
// Newer versions of Joi don't format the key for missing params the same way. This shim | ||
// provides backwards compatibility. Unfortunately, Joi doesn't export it's own Error class | ||
// in JS so we have to rely on the `name` key before we can cast it. | ||
// | ||
// The Hapi code we're 'overwriting' can be found here: | ||
// https://github.com/hapijs/hapi/blob/master/lib/validation.js#L102 | ||
if (err && err.name === 'ValidationError' && err.hasOwnProperty('output')) { | ||
const validationError: HapiValidationError = err as HapiValidationError; | ||
const validationKeys: string[] = []; | ||
|
||
validationError.details.forEach((detail) => { | ||
if (detail.path.length > 0) { | ||
validationKeys.push(Hoek.escapeHtml(detail.path.join('.'))); | ||
} else { | ||
// If no path, use the value sigil to signal the entire value had an issue. | ||
validationKeys.push('value'); | ||
} | ||
}); | ||
|
||
validationError.output.payload.validation.keys = validationKeys; | ||
} | ||
|
||
throw err; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { IHttpConfig } from './types'; | ||
|
||
export interface ListenerOptions { | ||
keepaliveTimeout: number; | ||
socketTimeout: number; | ||
} | ||
|
||
export function getListenerOptions(config: IHttpConfig): ListenerOptions { | ||
return { | ||
keepaliveTimeout: config.keepaliveTimeout, | ||
socketTimeout: config.socketTimeout, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { getRequestId } from './get_request_id'; | ||
|
||
jest.mock('uuid', () => ({ | ||
v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), | ||
})); | ||
|
||
describe('getRequestId', () => { | ||
describe('when allowFromAnyIp is true', () => { | ||
it('generates a UUID if no x-opaque-id header is present', () => { | ||
const request = { | ||
headers: {}, | ||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, | ||
} as any; | ||
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( | ||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' | ||
); | ||
}); | ||
|
||
it('uses x-opaque-id header value if present', () => { | ||
const request = { | ||
headers: { | ||
'x-opaque-id': 'id from header', | ||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, | ||
}, | ||
} as any; | ||
expect(getRequestId(request, { allowFromAnyIp: true, ipAllowlist: [] })).toEqual( | ||
'id from header' | ||
); | ||
}); | ||
}); | ||
|
||
describe('when allowFromAnyIp is false', () => { | ||
describe('and ipAllowlist is empty', () => { | ||
it('generates a UUID even if x-opaque-id header is present', () => { | ||
const request = { | ||
headers: { 'x-opaque-id': 'id from header' }, | ||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, | ||
} as any; | ||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: [] })).toEqual( | ||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' | ||
); | ||
}); | ||
}); | ||
|
||
describe('and ipAllowlist is not empty', () => { | ||
it('uses x-opaque-id header if request comes from trusted IP address', () => { | ||
const request = { | ||
headers: { 'x-opaque-id': 'id from header' }, | ||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, | ||
} as any; | ||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( | ||
'id from header' | ||
); | ||
}); | ||
|
||
it('generates a UUID if request comes from untrusted IP address', () => { | ||
const request = { | ||
headers: { 'x-opaque-id': 'id from header' }, | ||
raw: { req: { socket: { remoteAddress: '5.5.5.5' } } }, | ||
} as any; | ||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( | ||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' | ||
); | ||
}); | ||
|
||
it('generates UUID if request comes from trusted IP address but no x-opaque-id header is present', () => { | ||
const request = { | ||
headers: {}, | ||
raw: { req: { socket: { remoteAddress: '1.1.1.1' } } }, | ||
} as any; | ||
expect(getRequestId(request, { allowFromAnyIp: false, ipAllowlist: ['1.1.1.1'] })).toEqual( | ||
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' | ||
); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { Request } from '@hapi/hapi'; | ||
import uuid from 'uuid'; | ||
|
||
export function getRequestId( | ||
request: Request, | ||
options: { allowFromAnyIp: boolean; ipAllowlist: string[] } | ||
): string { | ||
return options.allowFromAnyIp || | ||
// socket may be undefined in integration tests that connect via the http listener directly | ||
(request.raw.req.socket?.remoteAddress && | ||
options.ipAllowlist.includes(request.raw.req.socket.remoteAddress)) | ||
? request.headers['x-opaque-id'] ?? uuid.v4() | ||
: uuid.v4(); | ||
} |
Oops, something went wrong.