[8.11] Add custom representation for request objects (#171155) (#171171)

# Backport

This will backport the following commits from `main` to `8.11`:
- [Add custom representation for request objects
(#171155)](https://github.com/elastic/kibana/pull/171155)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Pierre
Gayvallet","email":"pierre.gayvallet@elastic.co"},"sourceCommit":{"committedDate":"2023-11-14T11:16:32Z","message":"Add
custom representation for request objects (#171155)\n\n##
Summary\r\n\r\nAdd custom string/json/inspect representations for
`KibanaRequest`,\r\n`hapi.Request` and
`http.IncomingMessage`","sha":"4aa9ed52fd01ab25fe97bbd917f873e6da273ed0","branchLabelMapping":{"^v8.12.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:prev-minor","v8.12.0","v8.11.2"],"number":171155,"url":"https://github.com/elastic/kibana/pull/171155","mergeCommit":{"message":"Add
custom representation for request objects (#171155)\n\n##
Summary\r\n\r\nAdd custom string/json/inspect representations for
`KibanaRequest`,\r\n`hapi.Request` and
`http.IncomingMessage`","sha":"4aa9ed52fd01ab25fe97bbd917f873e6da273ed0"}},"sourceBranch":"main","suggestedTargetBranches":["8.11"],"targetPullRequestStates":[{"branch":"main","label":"v8.12.0","labelRegex":"^v8.12.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/171155","number":171155,"mergeCommit":{"message":"Add
custom representation for request objects (#171155)\n\n##
Summary\r\n\r\nAdd custom string/json/inspect representations for
`KibanaRequest`,\r\n`hapi.Request` and
`http.IncomingMessage`","sha":"4aa9ed52fd01ab25fe97bbd917f873e6da273ed0"}},{"branch":"8.11","label":"v8.11.2","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Pierre Gayvallet <pierre.gayvallet@elastic.co>
This commit is contained in:
Kibana Machine 2023-11-14 08:06:33 -05:00 committed by GitHub
parent 1adb557415
commit 9522ef2414
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 329 additions and 0 deletions

View file

@ -0,0 +1,53 @@
/*
* 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.
*/
// @ts-expect-error not in the definition file
import HapiRequest from '@hapi/hapi/lib/request.js';
import { IncomingMessage } from 'http';
import { inspect } from 'util';
export const patchRequest = () => {
// HAPI request
HapiRequest.prototype.toString = function () {
return `[HAPI.Request method="${this.method}" url="${this.url}"]`;
};
HapiRequest.prototype.toJSON = function () {
return {
method: this.method,
url: String(this.url),
};
};
HapiRequest.prototype[inspect.custom] = function () {
return this.toJSON();
};
// http.IncomingMessage
const IncomingMessageProto = IncomingMessage.prototype;
IncomingMessageProto.toString = function () {
return `[http.IncomingMessage method="${this.method}" url="${this.url}" complete="${this.complete}" aborted="${this.aborted}"]`;
};
// @ts-expect-error missing definition
IncomingMessageProto.toJSON = function () {
return {
method: this.method,
url: this.url,
complete: this.complete,
aborted: this.aborted,
};
};
// @ts-expect-error missing definition
IncomingMessageProto[inspect.custom] = function () {
// @ts-expect-error missing definition
return this.toJSON();
};
};

View file

@ -8,6 +8,7 @@
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import { inspect } from 'util';
import type { Request, RouteOptions } from '@hapi/hapi';
import { fromEvent, NEVER } from 'rxjs';
import { shareReplay, first, filter } from 'rxjs/operators';
@ -36,6 +37,10 @@ import {
import { RouteValidator } from './validator';
import { isSafeMethod } from './route';
import { KibanaSocket } from './socket';
import { patchRequest } from './patch_requests';
// patching at module load
patchRequest();
const requestSymbol = Symbol('request');
@ -185,6 +190,29 @@ export class CoreKibanaRequest<
};
}
toString() {
return `[CoreKibanaRequest id="${this.id}" method="${this.route.method}" url="${this.url}" fake="${this.isFakeRequest}" system="${this.isSystemRequest}" api="${this.isInternalApiRequest}"]`;
}
toJSON() {
return {
id: this.id,
uuid: this.uuid,
url: `${this.url}`,
isFakeRequest: this.isFakeRequest,
isSystemRequest: this.isSystemRequest,
isInternalApiRequest: this.isInternalApiRequest,
auth: {
isAuthenticated: this.auth.isAuthenticated,
},
route: this.route,
};
}
[inspect.custom]() {
return this.toJSON();
}
private getEvents(request: RawRequest): KibanaRequestEvents {
if (isFakeRawRequest(request)) {
return {

View file

@ -0,0 +1,248 @@
/*
* 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.
*/
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'),
}));
import supertest from 'supertest';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks';
import { contextServiceMock } from '@kbn/core-http-context-server-mocks';
import type { HttpService } from '@kbn/core-http-server-internal';
import { ensureRawRequest } from '@kbn/core-http-router-server-internal';
import { createHttpServer } from '@kbn/core-http-server-mocks';
import { inspect } from 'util';
let server: HttpService;
let logger: ReturnType<typeof loggingSystemMock.create>;
const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
context: contextSetup,
executionContext: executionContextServiceMock.createInternalSetupContract(),
};
beforeEach(async () => {
logger = loggingSystemMock.create();
server = createHttpServer({ logger });
await server.preboot({ context: contextServiceMock.createPrebootContract() });
});
afterEach(async () => {
await server.stop();
});
const replacePorts = (input: string): string => input.replace(/[:][0-9]+[/]/g, ':XXXX/');
describe('request logging', () => {
describe('KibanaRequest', () => {
it('has expected string representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
return res.ok({ body: { req: String(req) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(replacePorts(response.body.req)).toEqual(
`[CoreKibanaRequest id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" method="get" url="http://127.0.0.1:XXXX/" fake="false" system="false" api="false"]`
);
});
it('has expected JSON representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
return res.ok({ body: { req: JSON.stringify(req) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(JSON.parse(replacePorts(response.body.req))).toEqual({
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
url: 'http://127.0.0.1:XXXX/',
isFakeRequest: false,
isInternalApiRequest: false,
isSystemRequest: false,
auth: {
isAuthenticated: false,
},
route: {
method: 'get',
path: '/',
options: expect.any(Object),
},
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
});
});
it('has expected inspect representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
return res.ok({ body: { req: inspect(req) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(replacePorts(response.body.req)).toMatchInlineSnapshot(`
"{
id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
url: 'http://127.0.0.1:XXXX/',
isFakeRequest: false,
isSystemRequest: false,
isInternalApiRequest: false,
auth: { isAuthenticated: false },
route: {
path: '/',
method: 'get',
options: {
authRequired: true,
xsrfRequired: false,
access: 'internal',
tags: [],
timeout: [Object],
body: undefined
}
}
}"
`);
});
});
describe('HAPI request', () => {
it('has expected string representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
const rawRequest = ensureRawRequest(req);
return res.ok({ body: { req: String(rawRequest) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(replacePorts(response.body.req)).toEqual(
`[HAPI.Request method="get" url="http://127.0.0.1:XXXX/"]`
);
});
it('has expected JSON representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
const rawRequest = ensureRawRequest(req);
return res.ok({ body: { req: JSON.stringify(rawRequest) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(JSON.parse(replacePorts(response.body.req))).toEqual({
method: 'get',
url: 'http://127.0.0.1:XXXX/',
});
});
it('has expected inspect representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
const rawRequest = ensureRawRequest(req);
return res.ok({ body: { req: inspect(rawRequest) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(replacePorts(response.body.req)).toMatchInlineSnapshot(
`"{ method: 'get', url: 'http://127.0.0.1:XXXX/' }"`
);
});
});
describe('http.IncomingMessage', () => {
it('has expected string representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
const rawRawRequest = ensureRawRequest(req).raw.req;
return res.ok({ body: { req: String(rawRawRequest) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(replacePorts(response.body.req)).toEqual(
`[http.IncomingMessage method="GET" url="/" complete="true" aborted="false"]`
);
});
it('has expected JSON representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
const rawRawRequest = ensureRawRequest(req).raw.req;
return res.ok({ body: { req: JSON.stringify(rawRawRequest) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(JSON.parse(replacePorts(response.body.req))).toEqual({
aborted: false,
complete: true,
method: 'GET',
url: '/',
});
});
it('has expected inspect representation', async () => {
const { server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
router.get(
{ path: '/', validate: false, options: { authRequired: true } },
(context, req, res) => {
const rawRawRequest = ensureRawRequest(req).raw.req;
return res.ok({ body: { req: inspect(rawRawRequest) } });
}
);
await server.start();
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(replacePorts(response.body.req)).toMatchInlineSnapshot(
`"{ method: 'GET', url: '/', complete: true, aborted: false }"`
);
});
});
});