mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Simplify router for LensProxy
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
305c4a5573
commit
3f467b8fd3
@ -5,7 +5,6 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
import { kubeApiUpgradeRequest } from "./proxy-functions";
|
import { kubeApiUpgradeRequest } from "./proxy-functions";
|
||||||
import routerInjectable from "../router/router.injectable";
|
|
||||||
import httpProxy from "http-proxy";
|
import httpProxy from "http-proxy";
|
||||||
import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable";
|
import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable";
|
||||||
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
|
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
|
||||||
@ -14,12 +13,13 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.inject
|
|||||||
import loggerInjectable from "../../common/logger.injectable";
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
|
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
|
||||||
import getClusterForRequestInjectable from "./get-cluster-for-request.injectable";
|
import getClusterForRequestInjectable from "./get-cluster-for-request.injectable";
|
||||||
|
import routeRequestInjectable from "../router/route-request.injectable";
|
||||||
|
|
||||||
const lensProxyInjectable = getInjectable({
|
const lensProxyInjectable = getInjectable({
|
||||||
id: "lens-proxy",
|
id: "lens-proxy",
|
||||||
|
|
||||||
instantiate: (di) => new LensProxy({
|
instantiate: (di) => new LensProxy({
|
||||||
router: di.inject(routerInjectable),
|
routeRequest: di.inject(routeRequestInjectable),
|
||||||
proxy: httpProxy.createProxy(),
|
proxy: httpProxy.createProxy(),
|
||||||
kubeApiUpgradeRequest,
|
kubeApiUpgradeRequest,
|
||||||
shellApiRequest: di.inject(shellApiRequestInjectable),
|
shellApiRequest: di.inject(shellApiRequestInjectable),
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import https from "https";
|
|||||||
import type http from "http";
|
import type http from "http";
|
||||||
import type httpProxy from "http-proxy";
|
import type httpProxy from "http-proxy";
|
||||||
import { apiPrefix, apiKubePrefix } from "../../common/vars";
|
import { apiPrefix, apiKubePrefix } from "../../common/vars";
|
||||||
import type { Router } from "../router/router";
|
|
||||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||||
@ -18,6 +17,7 @@ import type { SetRequired } from "type-fest";
|
|||||||
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
|
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
|
||||||
import type { Logger } from "../../common/logger";
|
import type { Logger } from "../../common/logger";
|
||||||
import type { SelfSignedCert } from "selfsigned";
|
import type { SelfSignedCert } from "selfsigned";
|
||||||
|
import type { RouteRequest } from "../router/route-request.injectable";
|
||||||
|
|
||||||
export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
||||||
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
|
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
|
||||||
@ -28,7 +28,7 @@ interface Dependencies {
|
|||||||
shellApiRequest: LensProxyApiRequest;
|
shellApiRequest: LensProxyApiRequest;
|
||||||
kubeApiUpgradeRequest: LensProxyApiRequest;
|
kubeApiUpgradeRequest: LensProxyApiRequest;
|
||||||
emitAppEvent: EmitAppEvent;
|
emitAppEvent: EmitAppEvent;
|
||||||
readonly router: Router;
|
routeRequest: RouteRequest;
|
||||||
readonly proxy: httpProxy;
|
readonly proxy: httpProxy;
|
||||||
readonly lensProxyPort: { set: (portNumber: number) => void };
|
readonly lensProxyPort: { set: (portNumber: number) => void };
|
||||||
readonly contentSecurityPolicy: string;
|
readonly contentSecurityPolicy: string;
|
||||||
@ -247,6 +247,6 @@ export class LensProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader("Content-Security-Policy", this.dependencies.contentSecurityPolicy);
|
res.setHeader("Content-Security-Policy", this.dependencies.contentSecurityPolicy);
|
||||||
await this.dependencies.router.route(cluster, req, res);
|
await this.dependencies.routeRequest(cluster, req, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,73 +5,56 @@
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { ServerResponse } from "http";
|
import type { ServerResponse } from "http";
|
||||||
import loggerInjectable from "../../common/logger.injectable";
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
import { object } from "../../common/utils";
|
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
|
||||||
|
import authHeaderValueInjectable from "../lens-proxy/auth-header-value.injectable";
|
||||||
import type { LensApiRequest, Route } from "./route";
|
import type { LensApiRequest, Route } from "./route";
|
||||||
import { contentTypes } from "./router-content-types";
|
import { contentTypes } from "./router-content-types";
|
||||||
|
import { writeServerResponseFor } from "./write-server-response";
|
||||||
|
|
||||||
export type RouteHandler = (request: LensApiRequest<string>, response: ServerResponse) => Promise<void>;
|
export type RouteHandler = (request: LensApiRequest<string>, response: ServerResponse) => Promise<void>;
|
||||||
export type CreateHandlerForRoute = (route: Route<unknown, string>) => RouteHandler;
|
export type CreateHandlerForRoute = (route: Route<unknown, string>) => RouteHandler;
|
||||||
|
|
||||||
interface LensServerResponse {
|
|
||||||
statusCode: number;
|
|
||||||
content: any;
|
|
||||||
headers: {
|
|
||||||
[name: string]: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeServerResponseFor = (serverResponse: ServerResponse) => ({
|
|
||||||
statusCode,
|
|
||||||
content,
|
|
||||||
headers,
|
|
||||||
}: LensServerResponse) => {
|
|
||||||
serverResponse.statusCode = statusCode;
|
|
||||||
|
|
||||||
for (const [name, value] of object.entries(headers)) {
|
|
||||||
serverResponse.setHeader(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content instanceof Buffer) {
|
|
||||||
serverResponse.write(content);
|
|
||||||
serverResponse.end();
|
|
||||||
} else if (content) {
|
|
||||||
serverResponse.end(content);
|
|
||||||
} else {
|
|
||||||
serverResponse.end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createHandlerForRouteInjectable = getInjectable({
|
const createHandlerForRouteInjectable = getInjectable({
|
||||||
id: "create-handler-for-route",
|
id: "create-handler-for-route",
|
||||||
instantiate: (di): CreateHandlerForRoute => {
|
instantiate: (di): CreateHandlerForRoute => {
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const authHeaderValue = `Bearer ${di.inject(authHeaderValueInjectable)}`;
|
||||||
|
|
||||||
return (route) => async (request, response) => {
|
return (route) => async (request, response) => {
|
||||||
const writeServerResponse = writeServerResponseFor(response);
|
const writeServerResponse = writeServerResponseFor(response);
|
||||||
|
|
||||||
|
if (route.requireAuthentication) {
|
||||||
|
const authHeader = request.getHeader(lensAuthenticationHeader);
|
||||||
|
|
||||||
|
if (authHeader !== authHeaderValue) {
|
||||||
|
writeServerResponse(contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 401,
|
||||||
|
response: "Missing authorization",
|
||||||
|
}));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await route.handler(request);
|
const result = await route.handler(request);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const mappedResult = contentTypes.txt.resultMapper({
|
writeServerResponse(contentTypes.txt.resultMapper({
|
||||||
statusCode: 204,
|
statusCode: 204,
|
||||||
response: undefined,
|
response: undefined,
|
||||||
});
|
}));
|
||||||
|
|
||||||
writeServerResponse(mappedResult);
|
|
||||||
} else if (!result.proxy) {
|
} else if (!result.proxy) {
|
||||||
const contentType = result.contentType || contentTypes.json;
|
const contentType = result.contentType || contentTypes.json;
|
||||||
|
|
||||||
writeServerResponse(contentType.resultMapper(result));
|
writeServerResponse(contentType.resultMapper(result));
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
const mappedResult = contentTypes.txt.resultMapper({
|
logger.error(`[ROUTER]: route ${route.path}, called with ${request.path}, threw an error`, error);
|
||||||
|
writeServerResponse(contentTypes.txt.resultMapper({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
error: error ? String(error) : "unknown error",
|
error: error ? String(error) : "unknown error",
|
||||||
});
|
}));
|
||||||
|
|
||||||
logger.error(`[ROUTER]: route ${route.path}, called with ${request.path}, threw an error`, error);
|
|
||||||
writeServerResponse(mappedResult);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import Subtext from "@hapi/subtext";
|
|
||||||
|
|
||||||
export type ParseRequest = typeof Subtext.parse;
|
|
||||||
|
|
||||||
const parseRequestInjectable = getInjectable({
|
|
||||||
id: "parse-http-request",
|
|
||||||
instantiate: () => Subtext.parse,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default parseRequestInjectable;
|
|
||||||
81
src/main/router/route-request.injectable.ts
Normal file
81
src/main/router/route-request.injectable.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { DiContainerForInjection, Injectable, InjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
|
import type { Route, LensApiRequest } from "./route";
|
||||||
|
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
|
||||||
|
import Call from "@hapi/call";
|
||||||
|
import Subtext from "@hapi/subtext";
|
||||||
|
import type http from "http";
|
||||||
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
|
import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy";
|
||||||
|
import type { RouteHandler } from "./create-handler-for-route.injectable";
|
||||||
|
|
||||||
|
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
||||||
|
id: "route-injection-token",
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getRouteInjectable<T, Path extends string>(
|
||||||
|
opts: Omit<Injectable<Route<T, Path>, Route<T, Path>, void>, "lifecycle" | "injectionToken">,
|
||||||
|
) {
|
||||||
|
return getInjectable({
|
||||||
|
...opts,
|
||||||
|
injectionToken: routeInjectionToken as unknown as InjectionToken<Route<T, Path>, void>,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouteRequest = (cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse) => Promise<boolean>;
|
||||||
|
|
||||||
|
const createRouter = (di: DiContainerForInjection) => {
|
||||||
|
const routes = di.injectMany(routeInjectionToken);
|
||||||
|
const createHandlerForRoute = di.inject(createHandlerForRouteInjectable);
|
||||||
|
const router = new Call.Router<RouteHandler>();
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
router.add({ method: route.method, path: route.path }, createHandlerForRoute(route));
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeRequestInjectable = getInjectable({
|
||||||
|
id: "route-request",
|
||||||
|
instantiate: (di): RouteRequest => {
|
||||||
|
const router = createRouter(di);
|
||||||
|
|
||||||
|
return async (cluster, req, res) => {
|
||||||
|
const url = new URL(req.url, "https://localhost");
|
||||||
|
const path = url.pathname;
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
const matchingRoute = router.route(method, path);
|
||||||
|
|
||||||
|
if (matchingRoute instanceof Error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { payload } = await Subtext.parse(req, null, {
|
||||||
|
parse: true,
|
||||||
|
output: "data",
|
||||||
|
});
|
||||||
|
const request: LensApiRequest<string> = {
|
||||||
|
cluster,
|
||||||
|
path: url.pathname,
|
||||||
|
raw: {
|
||||||
|
req, res,
|
||||||
|
},
|
||||||
|
query: url.searchParams,
|
||||||
|
payload,
|
||||||
|
params: matchingRoute.params,
|
||||||
|
getHeader: (key) => req.headers[key.toLowerCase()],
|
||||||
|
};
|
||||||
|
|
||||||
|
await matchingRoute.route(request, res);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default routeRequestInjectable;
|
||||||
@ -35,6 +35,7 @@ export interface LensApiRequest<Path extends string> {
|
|||||||
params: InferParamFromPath<Path>;
|
params: InferParamFromPath<Path>;
|
||||||
cluster: Cluster | undefined;
|
cluster: Cluster | undefined;
|
||||||
query: URLSearchParams;
|
query: URLSearchParams;
|
||||||
|
getHeader: (key: string) => string | string[] | undefined;
|
||||||
raw: {
|
raw: {
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
res: http.ServerResponse;
|
res: http.ServerResponse;
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { Injectable, InjectionToken } from "@ogre-tools/injectable";
|
|
||||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
|
||||||
import { Router } from "./router";
|
|
||||||
import parseRequestInjectable from "./parse-request.injectable";
|
|
||||||
import type { Route } from "./route";
|
|
||||||
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
|
|
||||||
|
|
||||||
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
|
||||||
id: "route-injection-token",
|
|
||||||
});
|
|
||||||
|
|
||||||
export function getRouteInjectable<T, Path extends string>(
|
|
||||||
opts: Omit<Injectable<Route<T, Path>, Route<T, Path>, void>, "lifecycle" | "injectionToken">,
|
|
||||||
) {
|
|
||||||
return getInjectable({
|
|
||||||
...opts,
|
|
||||||
injectionToken: routeInjectionToken as unknown as InjectionToken<Route<T, Path>, void>,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const routerInjectable = getInjectable({
|
|
||||||
id: "router",
|
|
||||||
|
|
||||||
instantiate: (di) => new Router({
|
|
||||||
parseRequest: di.inject(parseRequestInjectable),
|
|
||||||
routes: di.injectMany(routeInjectionToken),
|
|
||||||
createHandlerForRoute: di.inject(createHandlerForRouteInjectable),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default routerInjectable;
|
|
||||||
@ -1,301 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import routerInjectable, { routeInjectionToken } from "./router.injectable";
|
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
|
||||||
import type { Router } from "./router";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import { Request } from "mock-http";
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import type { AsyncFnMock } from "@async-fn/jest";
|
|
||||||
import asyncFn from "@async-fn/jest";
|
|
||||||
import parseRequestInjectable from "./parse-request.injectable";
|
|
||||||
import { contentTypes } from "./router-content-types";
|
|
||||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
|
||||||
import type { Route } from "./route";
|
|
||||||
import type { SetRequired } from "type-fest";
|
|
||||||
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
|
|
||||||
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
|
|
||||||
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
|
|
||||||
import { runInAction } from "mobx";
|
|
||||||
|
|
||||||
describe("router", () => {
|
|
||||||
let router: Router;
|
|
||||||
let routeHandlerMock: AsyncFnMock<() => any>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
routeHandlerMock = asyncFn();
|
|
||||||
|
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
|
||||||
|
|
||||||
di.override(parseRequestInjectable, () => () => Promise.resolve({
|
|
||||||
payload: "some-payload",
|
|
||||||
mime: "some-mime",
|
|
||||||
}));
|
|
||||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
|
||||||
di.override(kubectlBinaryNameInjectable, () => "kubectl");
|
|
||||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
|
||||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
|
||||||
|
|
||||||
const injectable = getInjectable({
|
|
||||||
id: "some-route",
|
|
||||||
|
|
||||||
instantiate: () => ({
|
|
||||||
method: "get",
|
|
||||||
path: "/some-path",
|
|
||||||
handler: routeHandlerMock,
|
|
||||||
} as Route<any, string>),
|
|
||||||
|
|
||||||
injectionToken: routeInjectionToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
di.register(injectable);
|
|
||||||
});
|
|
||||||
|
|
||||||
router = di.inject(routerInjectable);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when navigating to the route", () => {
|
|
||||||
let actualPromise: Promise<boolean>;
|
|
||||||
let clusterStub: Cluster;
|
|
||||||
let requestStub: SetRequired<Request, "url" | "method">;
|
|
||||||
let responseStub: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
requestStub = new Request({
|
|
||||||
url: "/some-path",
|
|
||||||
method: "get",
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
}) as SetRequired<Request, "url" | "method">;
|
|
||||||
|
|
||||||
responseStub = { end: jest.fn(), setHeader: jest.fn(), write: jest.fn(), statusCode: undefined };
|
|
||||||
|
|
||||||
clusterStub = {} as Cluster;
|
|
||||||
|
|
||||||
actualPromise = router.route(clusterStub, requestStub, responseStub);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls handler with the request", () => {
|
|
||||||
expect(routeHandlerMock).toHaveBeenCalledWith({
|
|
||||||
cluster: clusterStub,
|
|
||||||
params: {},
|
|
||||||
path: "/some-path",
|
|
||||||
payload: "some-payload",
|
|
||||||
query: expect.any(URLSearchParams),
|
|
||||||
raw: { req: requestStub, res: responseStub },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("given no content-type is specified, when handler resolves, resolves with JSON", async () => {
|
|
||||||
await routeHandlerMock.resolve({ response: "some-response-from-route-handler" });
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", "application/json"],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("given JSON content-type is specified, when handler resolves with object, resolves with JSON", async () => {
|
|
||||||
await routeHandlerMock.resolve({ response: { some: "object" }});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.end).toHaveBeenCalledWith('{"some":"object"}');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when handler resolves without any result", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await routeHandlerMock.resolve(undefined);
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves as plain text", () => {
|
|
||||||
expect(responseStub.setHeader.mock.calls).toEqual([["Content-Type", "text/plain"]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves with status code for no content", async () => {
|
|
||||||
expect(responseStub.statusCode).toBe(204);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves without content", async () => {
|
|
||||||
expect(responseStub.end.mock.calls).toEqual([[]]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when handler rejects", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await routeHandlerMock.reject(new Error("some-error"));
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves as plain text", () => {
|
|
||||||
expect(responseStub.setHeader.mock.calls).toEqual([["Content-Type", "text/plain"]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resolves with "500" status code', () => {
|
|
||||||
expect(responseStub.statusCode).toBe(500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resolves with error", () => {
|
|
||||||
expect(responseStub.end).toHaveBeenCalledWith("Error: some-error");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
[
|
|
||||||
{ contentType: "text/plain", contentTypeObject: contentTypes.txt },
|
|
||||||
{ contentType: "application/json", contentTypeObject: contentTypes.json },
|
|
||||||
{ contentType: "text/html", contentTypeObject: contentTypes.html },
|
|
||||||
{ contentType: "text/css", contentTypeObject: contentTypes.css },
|
|
||||||
{ contentType: "image/gif", contentTypeObject: contentTypes.gif },
|
|
||||||
{ contentType: "image/jpeg", contentTypeObject: contentTypes.jpg },
|
|
||||||
{ contentType: "image/png", contentTypeObject: contentTypes.png },
|
|
||||||
{ contentType: "image/svg+xml", contentTypeObject: contentTypes.svg },
|
|
||||||
{ contentType: "application/javascript", contentTypeObject: contentTypes.js },
|
|
||||||
{ contentType: "font/woff2", contentTypeObject: contentTypes.woff2 },
|
|
||||||
{ contentType: "font/ttf", contentTypeObject: contentTypes.ttf },
|
|
||||||
].forEach(scenario => {
|
|
||||||
describe(`given content type is "${scenario.contentType}", when handler resolves with response`, () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await routeHandlerMock.resolve({ response: "some-response", contentType: scenario.contentTypeObject });
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has content type specific headers", () => {
|
|
||||||
expect(responseStub.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", scenario.contentType],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("defaults to successful status code", () => {
|
|
||||||
expect(responseStub.statusCode).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("has response as body", () => {
|
|
||||||
expect(responseStub.end).toHaveBeenCalledWith("some-response");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves with success and custom status code, defaults to "200" as status code`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
response: "some-response",
|
|
||||||
statusCode: 204,
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.statusCode).toBe(204);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves with success but without status code, defaults to "200" as status code`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
response: "some-response",
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.statusCode).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves without response, has no body`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
response: undefined,
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.end.mock.calls).toEqual([[]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves with error, has error as body`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
error: "some-error",
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.end).toHaveBeenCalledWith("some-error");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves with error and status code, has custom status code`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
error: "some-error",
|
|
||||||
statusCode: 414,
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.statusCode).toBe(414);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves with error but without status code, defaults to "400" as status code`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
error: "some-error",
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.statusCode).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`given content type is "${scenario.contentType}", when handler resolves custom headers, resolves with content type specific headers and custom headers`, async () => {
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
response: "irrelevant",
|
|
||||||
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "some-content-type-to-be-overridden",
|
|
||||||
"Some-Header": "some-header-value",
|
|
||||||
},
|
|
||||||
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
|
|
||||||
expect(responseStub.setHeader.mock.calls).toEqual([
|
|
||||||
["Content-Type", scenario.contentType],
|
|
||||||
["Some-Header", "some-header-value"],
|
|
||||||
]);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(`given content type is "${scenario.contentType}", when handler resolves with binary content`, () => {
|
|
||||||
let responseBufferStub: Buffer;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
responseBufferStub = Buffer.from("some-binary-content");
|
|
||||||
|
|
||||||
await routeHandlerMock.resolve({
|
|
||||||
response: responseBufferStub,
|
|
||||||
contentType: scenario.contentTypeObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
await actualPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("writes binary content to response", () => {
|
|
||||||
expect(responseStub.write).toHaveBeenCalledWith(responseBufferStub);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not end with the response", () => {
|
|
||||||
expect(responseStub.end.mock.calls[0]).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Call from "@hapi/call";
|
|
||||||
import type http from "http";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import type { LensApiRequest, Route } from "./route";
|
|
||||||
import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy";
|
|
||||||
import type { ParseRequest } from "./parse-request.injectable";
|
|
||||||
import type { CreateHandlerForRoute, RouteHandler } from "./create-handler-for-route.injectable";
|
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
|
||||||
req: http.IncomingMessage;
|
|
||||||
res: http.ServerResponse;
|
|
||||||
cluster: Cluster | undefined;
|
|
||||||
params: Partial<Record<string, string>>;
|
|
||||||
url: URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
parseRequest: ParseRequest;
|
|
||||||
createHandlerForRoute: CreateHandlerForRoute;
|
|
||||||
readonly routes: Route<unknown, string>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Router {
|
|
||||||
private readonly router = new Call.Router<RouteHandler>();
|
|
||||||
|
|
||||||
constructor(private readonly dependencies: Dependencies) {
|
|
||||||
for (const route of this.dependencies.routes) {
|
|
||||||
this.router.add({ method: route.method, path: route.path }, this.dependencies.createHandlerForRoute(route));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async route(cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
|
||||||
const url = new URL(req.url, "http://localhost");
|
|
||||||
const path = url.pathname;
|
|
||||||
const method = req.method.toLowerCase();
|
|
||||||
const matchingRoute = this.router.route(method, path);
|
|
||||||
|
|
||||||
if (matchingRoute instanceof Error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params });
|
|
||||||
|
|
||||||
await matchingRoute.route(request, res);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest<string>> {
|
|
||||||
const { req, res, url, cluster, params } = opts;
|
|
||||||
const { payload } = await this.dependencies.parseRequest(req, null, {
|
|
||||||
parse: true,
|
|
||||||
output: "data",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
cluster,
|
|
||||||
path: url.pathname,
|
|
||||||
raw: {
|
|
||||||
req, res,
|
|
||||||
},
|
|
||||||
query: url.searchParams,
|
|
||||||
payload,
|
|
||||||
params,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
src/main/router/write-server-response.ts
Normal file
36
src/main/router/write-server-response.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ServerResponse } from "http";
|
||||||
|
import { object } from "../../common/utils";
|
||||||
|
|
||||||
|
export interface LensServerResponse {
|
||||||
|
statusCode: number;
|
||||||
|
content: any;
|
||||||
|
headers: {
|
||||||
|
[name: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writeServerResponseFor = (serverResponse: ServerResponse) => ({
|
||||||
|
statusCode,
|
||||||
|
content,
|
||||||
|
headers,
|
||||||
|
}: LensServerResponse) => {
|
||||||
|
serverResponse.statusCode = statusCode;
|
||||||
|
|
||||||
|
for (const [name, value] of object.entries(headers)) {
|
||||||
|
serverResponse.setHeader(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content instanceof Buffer) {
|
||||||
|
serverResponse.write(content);
|
||||||
|
serverResponse.end();
|
||||||
|
} else if (content) {
|
||||||
|
serverResponse.end(content);
|
||||||
|
} else {
|
||||||
|
serverResponse.end();
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user