Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(router-proxy): added support for proxy endpoint #855

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ export class HttpServer {
},
});
}

for (const proxy of router.getProxies()) {
this.server.route(proxy);
}
}

await this.server.start();
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/http/router/router.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ function create({ routerPath = '' }: { routerPath?: string } = {}): RouterMock {
patch: jest.fn(),
getRoutes: jest.fn(),
handleLegacyErrors: jest.fn().mockImplementation((handler) => handler),
proxy: jest.fn(),
getProxies: jest.fn(),
};
}

Expand Down
94 changes: 93 additions & 1 deletion src/core/server/http/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@
* GitHub history for details.
*/

import { Request, ResponseObject, ResponseToolkit } from 'hapi';
import { Request, ResponseObject, ResponseToolkit, ServerRoute } from 'hapi';
import Boom from 'boom';

import { isConfigSchema } from '@osd/config-schema';
import { IncomingMessage } from 'http';
import { Logger } from '../../logging';
import { LegacyOpenSearchErrorHelpers } from '../../opensearch/legacy/errors';
import {
Expand All @@ -60,6 +61,25 @@ interface RouterRoute {
handler: (req: Request, responseToolkit: ResponseToolkit) => Promise<ResponseObject | Boom<any>>;
}

interface ProxyRouterExtraOptions {
proxyPath: string;
additionalHeaders?:
| Record<string, string>
| ((request: Request) => Promise<Record<string, string>>);
onResponse?: (err: null | Boom, res: IncomingMessage, req: Request) => IncomingMessage;
}

type RouterProxy = ServerRoute;

/**
* Proxy handler common definition
*
* @public
*/
export type ProxyRegistrar = <P, Q, B>(
route: RouteConfig<P, Q, B, any> & ProxyRouterExtraOptions
) => void;

/**
* Route handler common definition
*
Expand All @@ -82,6 +102,12 @@ export interface IRouter {
*/
routerPath: string;

/**
* Register a proxy handler for requests.
* @param route {@link RouteConfig} - a route configuration.
*/
proxy: ProxyRegistrar;

/**
* Register a route handler for `GET` request.
* @param route {@link RouteConfig} - a route configuration.
Expand Down Expand Up @@ -129,6 +155,13 @@ export interface IRouter {
* @internal
*/
getRoutes: () => RouterRoute[];

/**
* Returns all proxies registered with this router.
* @returns List of registered proxies.
* @internal
*/
getProxies: () => RouterProxy[];
}

export type ContextEnhancer<P, Q, B, Method extends RouteMethod> = (
Expand Down Expand Up @@ -218,11 +251,13 @@ function validOptions(
*/
export class Router implements IRouter {
public routes: Array<Readonly<RouterRoute>> = [];
public proxies: Array<Readonly<RouterProxy>> = [];
public get: IRouter['get'];
public post: IRouter['post'];
public delete: IRouter['delete'];
public put: IRouter['put'];
public patch: IRouter['patch'];
public proxy: IRouter['proxy'];

constructor(
public readonly routerPath: string,
Expand All @@ -249,17 +284,74 @@ export class Router implements IRouter {
});
};

const buildProxy = () => <P, Q, B>(
proxyConfig: RouteConfig<P, Q, B, any> & ProxyRouterExtraOptions
) => {
const methods: RouteMethod[] = ['get', 'post', 'delete', 'put', 'patch', 'options'];
const proxyPath =
proxyConfig.proxyPath +
(proxyConfig.proxyPath.charAt(proxyConfig.proxyPath.length - 1) === '/' ? '' : '/');

methods.forEach((method) => {
this.proxies.push({
path: `${proxyConfig.path}/{path*}`,
method,
options:
method === 'get'
? undefined
: {
payload: { parse: false },
validate: { payload: true },
},
handler: {
proxy: {
onResponse: (err, res, req) => {
if (proxyConfig.onResponse) {
return proxyConfig.onResponse(err, res, req);
}
return res;
},
mapUri: async (request) => {
let additionalHeaders;

if (typeof proxyConfig.additionalHeaders === 'function') {
additionalHeaders = await proxyConfig.additionalHeaders(request);
} else {
additionalHeaders = proxyConfig.additionalHeaders;
}

return {
uri: proxyPath + request.params.path,
headers: {
...request.headers,
...additionalHeaders,
},
};
},
passThrough: true,
xforward: true,
},
},
});
});
};

this.get = buildMethod('get');
this.post = buildMethod('post');
this.delete = buildMethod('delete');
this.put = buildMethod('put');
this.patch = buildMethod('patch');
this.proxy = buildProxy();
}

public getRoutes() {
return [...this.routes];
}

public getProxies() {
return [...this.proxies];
}

public handleLegacyErrors = wrapErrors;

private async handle<P, Q, B>({
Expand Down