From 7cbea0c7f45b760415cb79e0e27a31f4ba92c24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Ferna=CC=81ndez=20Haro?= Date: Fri, 15 Nov 2019 13:51:10 +0000 Subject: [PATCH] Allow routes to define some payload config values --- .../kibana-plugin-server.irouter.delete.md | 2 +- .../kibana-plugin-server.irouter.get.md | 2 +- .../server/kibana-plugin-server.irouter.md | 8 +-- .../kibana-plugin-server.irouter.post.md | 2 +- .../kibana-plugin-server.irouter.put.md | 2 +- .../kibana-plugin-server.kibanarequest.md | 2 +- ...ibana-plugin-server.kibanarequest.route.md | 2 +- ...kibana-plugin-server.kibanarequestroute.md | 6 +- ...plugin-server.kibanarequestroute.method.md | 2 +- ...lugin-server.kibanarequestroute.options.md | 2 +- .../core/server/kibana-plugin-server.md | 1 + .../kibana-plugin-server.routeconfig.md | 4 +- ...ibana-plugin-server.routeconfig.options.md | 2 +- ...lugin-server.routeconfigoptions.accepts.md | 13 ++++ ...ugin-server.routeconfigoptions.maxbytes.md | 13 ++++ ...kibana-plugin-server.routeconfigoptions.md | 6 +- ...plugin-server.routeconfigoptions.output.md | 13 ++++ ...-plugin-server.routeconfigoptions.parse.md | 13 ++++ .../kibana-plugin-server.routecontenttype.md | 13 ++++ src/core/server/http/http_server.test.ts | 32 +++++++++ src/core/server/http/http_server.ts | 15 +++- src/core/server/http/index.ts | 1 + src/core/server/http/router/index.ts | 2 +- src/core/server/http/router/request.ts | 16 +++-- src/core/server/http/router/route.ts | 69 ++++++++++++++++++- src/core/server/http/router/router.ts | 19 ++--- src/core/server/index.ts | 1 + src/core/server/server.api.md | 29 +++++--- .../server/routes/authentication/saml.test.ts | 2 +- 29 files changed, 244 insertions(+), 50 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptions.accepts.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptions.maxbytes.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptions.output.md create mode 100644 docs/development/core/server/kibana-plugin-server.routeconfigoptions.parse.md create mode 100644 docs/development/core/server/kibana-plugin-server.routecontenttype.md diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md index 9124b4a1b21c4c2..46b6d9db2efcf45 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.delete.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -9,5 +9,5 @@ Register a route handler for `DELETE` request. Signature: ```typescript -delete:

(route: RouteConfig, handler: RequestHandler) => void; +delete:

(route: RouteConfig, handler: RequestHandler) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md index 0291906c6fc6b90..5c072bb5d91f9e9 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.get.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -9,5 +9,5 @@ Register a route handler for `GET` request. Signature: ```typescript -get:

(route: RouteConfig, handler: RequestHandler) => void; +get:

(route: RouteConfig, handler: RequestHandler) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md index bbffe1e42f229a5..9f64cc93a0383a5 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -16,9 +16,9 @@ export interface IRouter | Property | Type | Description | | --- | --- | --- | -| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | -| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | -| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | -| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | +| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B, 'delete'>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B, 'get'>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | +| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B, 'post'>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B, 'put'>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | | [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md index e97a32e433ce951..f6e71c99fcf15a8 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.post.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -9,5 +9,5 @@ Register a route handler for `POST` request. Signature: ```typescript -post:

(route: RouteConfig, handler: RequestHandler) => void; +post:

(route: RouteConfig, handler: RequestHandler) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md index 25db91e3899397e..d552983dc687121 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.put.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -9,5 +9,5 @@ Register a route handler for `PUT` request. Signature: ```typescript -put:

(route: RouteConfig, handler: RequestHandler) => void; +put:

(route: RouteConfig, handler: RequestHandler) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index b2460cd58f7a724..2482b72c6e3dd5a 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -26,7 +26,7 @@ export declare class KibanaRequestHeaders | Readonly copy of incoming request headers. | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | -| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | matched route details | +| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<RouteMethod | 'patch' | 'options'>> | matched route details | | [socket](./kibana-plugin-server.kibanarequest.socket.md) | | IKibanaSocket | | | [url](./kibana-plugin-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md index 88954eedf4cfb50..5b1bfa70e8786dc 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.route.md @@ -9,5 +9,5 @@ matched route details Signature: ```typescript -readonly route: RecursiveReadonly; +readonly route: RecursiveReadonly>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md index b92fe45d19edb81..6b4bbb7255cb46a 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.md @@ -9,14 +9,14 @@ Request specific route information exposed to a handler. Signature: ```typescript -export interface KibanaRequestRoute +export interface KibanaRequestRoute ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [method](./kibana-plugin-server.kibanarequestroute.method.md) | RouteMethod | 'patch' | 'options' | | -| [options](./kibana-plugin-server.kibanarequestroute.options.md) | Required<RouteConfigOptions> | | +| [method](./kibana-plugin-server.kibanarequestroute.method.md) | Method | | +| [options](./kibana-plugin-server.kibanarequestroute.options.md) | Method extends 'get' ? Required<Pick<RouteConfigOptions<Method>, 'authRequired' | 'tags'>> : Required<RouteConfigOptions> | | | [path](./kibana-plugin-server.kibanarequestroute.path.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md index c003b06e128e431..5775d28b1e053b2 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.method.md @@ -7,5 +7,5 @@ Signature: ```typescript -method: RouteMethod | 'patch' | 'options'; +method: Method; ``` diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md index 98c898449a5b1df..f67ece968f6d943 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequestroute.options.md @@ -7,5 +7,5 @@ Signature: ```typescript -options: Required; +options: Method extends 'get' ? Required, 'authRequired' | 'tags'>> : Required; ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9907750b8742f03..3aafbed10788aa4 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -167,6 +167,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | +| [RouteContentType](./kibana-plugin-server.routecontenttype.md) | The set of supported parseable Content-Types | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.md b/docs/development/core/server/kibana-plugin-server.routeconfig.md index 769d0dda4264478..cb0b7ae9f435b53 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.md @@ -9,14 +9,14 @@ Route specific configuration. Signature: ```typescript -export interface RouteConfig

+export interface RouteConfig

``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [options](./kibana-plugin-server.routeconfig.options.md) | RouteConfigOptions | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | +| [options](./kibana-plugin-server.routeconfig.options.md) | RouteConfigOptions<Method> | Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md). | | [path](./kibana-plugin-server.routeconfig.path.md) | string | The endpoint \_within\_ the router path to register the route. | | [validate](./kibana-plugin-server.routeconfig.validate.md) | RouteSchemas<P, Q, B> | false | A schema created with @kbn/config-schema that every request will be validated against. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.options.md b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md index 12ca36da6de7cbb..90ad294457101fb 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.options.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.options.md @@ -9,5 +9,5 @@ Additional route options [RouteConfigOptions](./kibana-plugin-server.routeconfig Signature: ```typescript -options?: RouteConfigOptions; +options?: RouteConfigOptions; ``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.accepts.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.accepts.md new file mode 100644 index 000000000000000..a6b1cf2da07fea3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.accepts.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [accepts](./kibana-plugin-server.routeconfigoptions.accepts.md) + +## RouteConfigOptions.accepts property + +Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response. + +Signature: + +```typescript +accepts?: Method extends 'get' ? never : RouteContentType | RouteContentType[] | string | string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.maxbytes.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.maxbytes.md new file mode 100644 index 000000000000000..34a12c616aa98a8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.maxbytes.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [maxBytes](./kibana-plugin-server.routeconfigoptions.maxbytes.md) + +## RouteConfigOptions.maxBytes property + +Default value: The one set in the kibana.yml config file under the parameter `server.maxPayloadBytes`. Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory. + +Signature: + +```typescript +maxBytes?: Method extends 'get' ? never : number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md index b4d210ac0b7110d..e374c3444776618 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.md @@ -9,13 +9,17 @@ Additional route options. Signature: ```typescript -export interface RouteConfigOptions +export interface RouteConfigOptions ``` ## Properties | Property | Type | Description | | --- | --- | --- | +| [accepts](./kibana-plugin-server.routeconfigoptions.accepts.md) | Method extends 'get' ? never : RouteContentType | RouteContentType[] | string | string[] | Default value: allows parsing of the following mime types: \* application/json \* application/\*+json \* application/octet-stream \* application/x-www-form-urlencoded \* multipart/form-data \* text/\* A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed above will not enable them to be parsed, and if parse is true, the request will result in an error response. | | [authRequired](./kibana-plugin-server.routeconfigoptions.authrequired.md) | boolean | A flag shows that authentication for a route: enabled when true disabled when falseEnabled by default. | +| [maxBytes](./kibana-plugin-server.routeconfigoptions.maxbytes.md) | Method extends 'get' ? never : number | Default value: The one set in the kibana.yml config file under the parameter server.maxPayloadBytes. Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory. | +| [output](./kibana-plugin-server.routeconfigoptions.output.md) | Method extends 'get' ? never : 'data' | 'stream' | 'file' | Default value: 'data'. The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire mutlipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez). \* 'file' - the incoming payload is written to temporary file in the directory specified by the uploads settings. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are saved to disk. Note that it is the sole responsibility of the application to clean up the files generated by the framework. This can be done by keeping track of which files are used (e.g. using the request.app object), and listening to the server 'response' event to perform cleanup. | +| [parse](./kibana-plugin-server.routeconfigoptions.parse.md) | Method extends 'get' ? never : boolean | 'gunzip' | Default value: true. Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded. | | [tags](./kibana-plugin-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.output.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.output.md new file mode 100644 index 000000000000000..99c39fbfc9c8dab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.output.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [output](./kibana-plugin-server.routeconfigoptions.output.md) + +## RouteConfigOptions.output property + +Default value: 'data'. The processed payload format. The value must be one of: \* 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw Buffer is returned. \* 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart payloads are a synthetic interface created on top of the entire mutlipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the multipart payload in the handler using a streaming parser (e.g. pez). \* 'file' - the incoming payload is written to temporary file in the directory specified by the uploads settings. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files are saved to disk. Note that it is the sole responsibility of the application to clean up the files generated by the framework. This can be done by keeping track of which files are used (e.g. using the request.app object), and listening to the server 'response' event to perform cleanup. + +Signature: + +```typescript +output?: Method extends 'get' ? never : 'data' | 'stream' | 'file'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routeconfigoptions.parse.md b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.parse.md new file mode 100644 index 000000000000000..4338713d47d8179 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routeconfigoptions.parse.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) > [parse](./kibana-plugin-server.routeconfigoptions.parse.md) + +## RouteConfigOptions.parse property + +Default value: true. Determines if the incoming payload is processed or presented raw. Available values: \* true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. \* false - the raw payload is returned unmodified. \* 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded. + +Signature: + +```typescript +parse?: Method extends 'get' ? never : boolean | 'gunzip'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routecontenttype.md b/docs/development/core/server/kibana-plugin-server.routecontenttype.md new file mode 100644 index 000000000000000..8e5b5aeaca66cd3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.routecontenttype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteContentType](./kibana-plugin-server.routecontenttype.md) + +## RouteContentType type + +The set of supported parseable Content-Types + +Signature: + +```typescript +export declare type RouteContentType = 'application/json' | 'application/*+json' | 'application/octec-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*'; +``` diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index acae9d8ff0e704e..39fc903c24c171b 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -577,6 +577,38 @@ test('exposes route details of incoming request to a route handler', async () => }); }); +test('exposes route details of incoming request to a route handler (POST + payload options)', async () => { + const { registerRouter, server: innerServer } = await server.setup(config); + + const router = new Router('', logger, enhanceWithContext); + router.post( + { + path: '/', + validate: false, + options: { accepts: 'application/json' }, + }, + (context, req, res) => res.ok({ body: req.route }) + ); + registerRouter(router); + + await server.start(); + await supertest(innerServer.listener) + .post('/') + .send({ test: 1 }) + .expect(200, { + method: 'post', + path: '/', + options: { + authRequired: true, + tags: [], + parse: true, // hapi populates the default + maxBytes: 1024, // hapi populates the default + accepts: ['application/json'], + output: 'data', + }, + }); +}); + describe('setup contract', () => { describe('#createSessionStorage', () => { it('creates session storage factory', async () => { diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index da97ab535516c8b..26c6fed9d06e28b 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -127,21 +127,32 @@ export class HttpServer { for (const router of this.registeredRouters) { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); - const { authRequired = true, tags } = route.options; // Hapi does not allow payload validation to be specified for 'head' or 'get' requests const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; + const { + authRequired = true, + tags, + accepts: allow, + maxBytes, + output, + parse, + } = route.options; this.server.route({ handler: route.handler, method: route.method, path: route.path, options: { - auth: authRequired ? undefined : false, + // Enforcing the comparison with true because plugins could overwrite the auth strategy by doing `options: { authRequired: authStrategy as any }` + auth: authRequired === true ? undefined : false, tags: tags ? Array.from(tags) : undefined, // TODO: This 'validate' section can be removed once the legacy platform is completely removed. // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default // validation applied in ./http_tools#getServerOptions // (All NP routes are already required to specify their own validation in order to access the payload) validate, + payload: [allow, maxBytes, output, parse].some(v => typeof v !== 'undefined') + ? { allow, maxBytes, output, parse } + : undefined, }, }); } diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index ff1ff3acfae3d05..98d0ff9035f0b8d 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -45,6 +45,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteContentType, } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index 56ed9ca11edc133..cb829b3012c1e79 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -26,7 +26,7 @@ export { LegacyRequest, ensureRawRequest, } from './request'; -export { RouteMethod, RouteConfig, RouteConfigOptions } from './route'; +export { RouteMethod, RouteConfig, RouteConfigOptions, RouteContentType } from './route'; export { HapiResponseAdapter } from './response_adapter'; export { CustomHttpResponseOptions, diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 5d3b70ba27eeefe..ed1b5f300535329 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -33,10 +33,12 @@ const requestSymbol = Symbol('request'); * Request specific route information exposed to a handler. * @public * */ -export interface KibanaRequestRoute { +export interface KibanaRequestRoute { path: string; - method: RouteMethod | 'patch' | 'options'; - options: Required; + method: Method; + options: Method extends 'get' + ? Required, 'authRequired' | 'tags'>> // GET requests cannot have payload options + : Required; // Using any because 'patch' and 'options' are not valid RouteMethods but shouldn't affect much to typings } /** @@ -113,7 +115,7 @@ export class KibanaRequest { /** a WHATWG URL standard object. */ public readonly url: Url; /** matched route details */ - public readonly route: RecursiveReadonly; + public readonly route: RecursiveReadonly>; /** * Readonly copy of incoming request headers. * @remarks @@ -150,12 +152,18 @@ export class KibanaRequest { private getRouteInfo() { const request = this[requestSymbol]; + const payload = request.route.settings.payload || {}; + const { parse, maxBytes, allow, output } = payload; return { path: request.path, method: request.method, options: { authRequired: request.route.settings.auth !== false, tags: request.route.settings.tags || [], + parse, + maxBytes, + accepts: allow, + output, }, }; } diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index bffa23551dd52de..cb9f3b5bc5440d4 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -24,11 +24,23 @@ import { ObjectType } from '@kbn/config-schema'; */ export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; +/** + * The set of supported parseable Content-Types + * @public + */ +export type RouteContentType = + | 'application/json' + | 'application/*+json' + | 'application/octec-stream' + | 'application/x-www-form-urlencoded' + | 'multipart/form-data' + | 'text/*'; + /** * Additional route options. * @public */ -export interface RouteConfigOptions { +export interface RouteConfigOptions { /** * A flag shows that authentication for a route: * `enabled` when true @@ -42,13 +54,64 @@ export interface RouteConfigOptions { * Additional metadata tag strings to attach to the route. */ tags?: readonly string[]; + + /** + * Default value: allows parsing of the following mime types: + * * application/json + * * application/*+json + * * application/octet-stream + * * application/x-www-form-urlencoded + * * multipart/form-data + * * text/* + * A string or an array of strings with the allowed mime types for the endpoint. Use this settings to limit the set of allowed mime types. Note that allowing additional mime types not listed + * above will not enable them to be parsed, and if parse is true, the request will result in an error response. + */ + accepts?: Method extends 'get' + ? never + : RouteContentType | RouteContentType[] | string | string[]; + + /** + * Default value: The one set in the kibana.yml config file under the parameter `server.maxPayloadBytes`. + * Limits the size of incoming payloads to the specified byte count. Allowing very large payloads may cause the server to run out of memory. + */ + maxBytes?: Method extends 'get' ? never : number; + + /** + * Default value: 'data'. + * The processed payload format. The value must be one of: + * * 'data' - the incoming payload is read fully into memory. If parse is true, the payload is parsed (JSON, form-decoded, multipart) based on the 'Content-Type' header. If parse is false, a raw + * Buffer is returned. + * * 'stream' - the incoming payload is made available via a Stream.Readable interface. If the payload is 'multipart/form-data' and parse is true, field values are presented as text while files + * are provided as streams. File streams from a 'multipart/form-data' upload will also have a hapi property containing the filename and headers properties. Note that payload streams for multipart + * payloads are a synthetic interface created on top of the entire mutlipart content loaded into memory. To avoid loading large multipart payloads into memory, set parse to false and handle the + * multipart payload in the handler using a streaming parser (e.g. pez). + * * 'file' - the incoming payload is written to temporary file in the directory specified by the uploads settings. If the payload is 'multipart/form-data' and parse is true, field values are + * presented as text while files are saved to disk. Note that it is the sole responsibility of the application to clean up the files generated by the framework. This can be done by keeping track + * of which files are used (e.g. using the request.app object), and listening to the server 'response' event to perform cleanup. + */ + output?: Method extends 'get' ? never : 'data' | 'stream' | 'file'; + + /** + * Default value: true. + * Determines if the incoming payload is processed or presented raw. Available values: + * * true - if the request 'Content-Type' matches the allowed mime types set by allow (for the whole payload as well as parts), the payload is converted into an object when possible. If the + * format is unknown, a Bad Request (400) error response is sent. Any known content encoding is decoded. + * * false - the raw payload is returned unmodified. + * * 'gunzip' - the raw payload is returned unmodified after any known content encoding is decoded. + */ + parse?: Method extends 'get' ? never : boolean | 'gunzip'; } /** * Route specific configuration. * @public */ -export interface RouteConfig

{ +export interface RouteConfig< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType, + Method extends RouteMethod +> { /** * The endpoint _within_ the router path to register the route. * @@ -125,7 +188,7 @@ export interface RouteConfig

; } /** diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 6b7e2e3ad14cd38..70257283c7552b2 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -53,7 +53,7 @@ export interface IRouter { * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ get:

( - route: RouteConfig, + route: RouteConfig, handler: RequestHandler ) => void; @@ -63,7 +63,7 @@ export interface IRouter { * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ post:

( - route: RouteConfig, + route: RouteConfig, handler: RequestHandler ) => void; @@ -73,7 +73,7 @@ export interface IRouter { * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ put:

( - route: RouteConfig, + route: RouteConfig, handler: RequestHandler ) => void; @@ -83,12 +83,12 @@ export interface IRouter { * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ delete:

( - route: RouteConfig, + route: RouteConfig, handler: RequestHandler ) => void; /** - * Returns all routes registered with the this router. + * Returns all routes registered with this router. * @returns List of registered routes. * @internal */ @@ -115,8 +115,9 @@ function getRouteFullPath(routerPath: string, routePath: string) { function routeSchemasFromRouteConfig< P extends ObjectType, Q extends ObjectType, - B extends ObjectType ->(route: RouteConfig, routeMethod: RouteMethod) { + B extends ObjectType, + Method extends RouteMethod +>(route: RouteConfig, routeMethod: Method) { // The type doesn't allow `validate` to be undefined, but it can still // happen when it's used from JavaScript. if (route.validate === undefined) { @@ -153,12 +154,12 @@ export class Router implements IRouter { private readonly log: Logger, private readonly enhanceWithContext: ContextEnhancer ) { - const buildMethod = (method: RouteMethod) => < + const buildMethod = (method: Method) => < P extends ObjectType, Q extends ObjectType, B extends ObjectType >( - route: RouteConfig, + route: RouteConfig, handler: RequestHandler ) => { const { path, options = {} } = route; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2a5631ad1c38017..38112021606a46d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -114,6 +114,7 @@ export { IRouter, RouteMethod, RouteConfigOptions, + RouteContentType, SessionStorage, SessionStorageCookieOptions, SessionStorageFactory, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 97a04a4a4efaba1..a73cbc7bd836fe1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -714,14 +714,14 @@ export interface IndexSettingsDeprecationInfo { // @public export interface IRouter { - delete:

(route: RouteConfig, handler: RequestHandler) => void; - get:

(route: RouteConfig, handler: RequestHandler) => void; + delete:

(route: RouteConfig, handler: RequestHandler) => void; + get:

(route: RouteConfig, handler: RequestHandler) => void; // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts // // @internal getRoutes: () => RouterRoute[]; - post:

(route: RouteConfig, handler: RequestHandler) => void; - put:

(route: RouteConfig, handler: RequestHandler) => void; + post:

(route: RouteConfig, handler: RequestHandler) => void; + put:

(route: RouteConfig, handler: RequestHandler) => void; routerPath: string; } @@ -760,18 +760,18 @@ export class KibanaRequest { readonly params: Params; // (undocumented) readonly query: Query; - readonly route: RecursiveReadonly; + readonly route: RecursiveReadonly>; // (undocumented) readonly socket: IKibanaSocket; readonly url: Url; } // @public -export interface KibanaRequestRoute { +export interface KibanaRequestRoute { // (undocumented) - method: RouteMethod | 'patch' | 'options'; + method: Method; // (undocumented) - options: Required; + options: Method extends 'get' ? Required, 'authRequired' | 'tags'>> : Required; // (undocumented) path: string; } @@ -1072,18 +1072,25 @@ export type ResponseHeaders = { }; // @public -export interface RouteConfig

{ - options?: RouteConfigOptions; +export interface RouteConfig

{ + options?: RouteConfigOptions; path: string; validate: RouteSchemas | false; } // @public -export interface RouteConfigOptions { +export interface RouteConfigOptions { + accepts?: Method extends 'get' ? never : RouteContentType | RouteContentType[] | string | string[]; authRequired?: boolean; + maxBytes?: Method extends 'get' ? never : number; + output?: Method extends 'get' ? never : 'data' | 'stream' | 'file'; + parse?: Method extends 'get' ? never : boolean | 'gunzip'; tags?: readonly string[]; } +// @public +export type RouteContentType = 'application/json' | 'application/*+json' | 'application/octec-stream' | 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/*'; + // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index cdef1826ddaa889..5b833453e11522c 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -42,7 +42,7 @@ describe('SAML authentication routes', () => { describe('Assertion consumer service endpoint', () => { let routeHandler: RequestHandler; - let routeConfig: RouteConfig; + let routeConfig: RouteConfig; beforeEach(() => { const [acsRouteConfig, acsRouteHandler] = router.post.mock.calls.find( ([{ path }]) => path === '/api/security/saml/callback'