mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Make code cleaner
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
9117bc2760
commit
adde13237a
@ -334,6 +334,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||||
"@typescript-eslint/parser": "^5.10.1",
|
"@typescript-eslint/parser": "^5.10.1",
|
||||||
"ansi_up": "^5.1.0",
|
"ansi_up": "^5.1.0",
|
||||||
|
"mock-http": "^1.1.0",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"circular-dependency-plugin": "^5.2.2",
|
"circular-dependency-plugin": "^5.2.2",
|
||||||
"cli-progress": "^3.10.0",
|
"cli-progress": "^3.10.0",
|
||||||
|
|||||||
@ -35,32 +35,21 @@ describe("Router", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("blocks path traversal attacks", async () => {
|
it("blocks path traversal attacks", async () => {
|
||||||
const response: any = {
|
|
||||||
statusCode: 200,
|
|
||||||
end: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const request = {
|
const request = {
|
||||||
params: {
|
params: {
|
||||||
path: "../index.ts",
|
path: "../index.ts",
|
||||||
},
|
},
|
||||||
response,
|
response: {},
|
||||||
raw: {},
|
raw: {},
|
||||||
} as LensApiRequest<any>;
|
} as LensApiRequest<any>;
|
||||||
|
|
||||||
await handleStaticFileRoute.handler(request);
|
const result = await handleStaticFileRoute.handler(request);
|
||||||
|
|
||||||
expect(response.statusCode).toEqual(404);
|
expect(result).toEqual({ statusCode: 404 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves files under static root", async () => {
|
it("serves files under static root", async () => {
|
||||||
const response: any = {
|
|
||||||
statusCode: 200,
|
|
||||||
write: jest.fn(),
|
|
||||||
setHeader: jest.fn(),
|
|
||||||
end: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const req: any = {
|
const req: any = {
|
||||||
url: "",
|
url: "",
|
||||||
};
|
};
|
||||||
@ -69,12 +58,12 @@ describe("Router", () => {
|
|||||||
params: {
|
params: {
|
||||||
path: "router.test.ts",
|
path: "router.test.ts",
|
||||||
},
|
},
|
||||||
response,
|
response: {},
|
||||||
raw: { req },
|
raw: { req },
|
||||||
} as LensApiRequest<any>;
|
} as LensApiRequest<any>;
|
||||||
|
|
||||||
await handleStaticFileRoute.handler(request);
|
const result = await handleStaticFileRoute.handler(request);
|
||||||
|
|
||||||
expect(response.statusCode).toEqual(200);
|
expect(result).toEqual({ statusCode: 404 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
86
src/main/router-content-types.ts
Normal file
86
src/main/router-content-types.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LensApiResult } from "./router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
export interface LensApiResultContentType {
|
||||||
|
resultMapper: (result: LensApiResult<any>) => ({ statusCode: number, content: any, headers: { [ name: string ]: string }})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultMapperFor =
|
||||||
|
(contentType: string): LensApiResultContentType["resultMapper"] =>
|
||||||
|
({ response, error, statusCode= error ? 400 : 200, headers = {}}) => ({
|
||||||
|
statusCode,
|
||||||
|
content: error || response,
|
||||||
|
headers: { ...headers, "Content-Type": contentType },
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SupportedFileExtension = "json" | "txt" | "html" | "css" | "gif" | "jpg" | "png" | "svg" | "js" | "woff2" | "ttf";
|
||||||
|
|
||||||
|
export const contentTypes: Record<SupportedFileExtension, LensApiResultContentType> = {
|
||||||
|
json: {
|
||||||
|
resultMapper: (result) => {
|
||||||
|
const resultMapper = resultMapperFor("application/json");
|
||||||
|
|
||||||
|
const mappedResult = resultMapper(result);
|
||||||
|
|
||||||
|
const contentIsObject = typeof mappedResult.content === "object";
|
||||||
|
const contentIsBuffer = mappedResult.content instanceof Buffer;
|
||||||
|
const contentShouldBeStringified = contentIsObject && !contentIsBuffer;
|
||||||
|
|
||||||
|
const content = contentShouldBeStringified
|
||||||
|
? JSON.stringify(mappedResult.content)
|
||||||
|
: mappedResult.content;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mappedResult,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
txt: {
|
||||||
|
resultMapper: resultMapperFor("text/plain"),
|
||||||
|
},
|
||||||
|
|
||||||
|
html: {
|
||||||
|
resultMapper: resultMapperFor("text/html"),
|
||||||
|
},
|
||||||
|
|
||||||
|
css: {
|
||||||
|
resultMapper: resultMapperFor("text/css"),
|
||||||
|
},
|
||||||
|
|
||||||
|
gif: {
|
||||||
|
resultMapper: resultMapperFor("image/gif"),
|
||||||
|
},
|
||||||
|
|
||||||
|
jpg: {
|
||||||
|
resultMapper: resultMapperFor("image/jpeg"),
|
||||||
|
},
|
||||||
|
|
||||||
|
png: {
|
||||||
|
resultMapper: resultMapperFor("image/png"),
|
||||||
|
},
|
||||||
|
|
||||||
|
svg: {
|
||||||
|
resultMapper: resultMapperFor("image/svg+xml"),
|
||||||
|
},
|
||||||
|
|
||||||
|
js: {
|
||||||
|
resultMapper: resultMapperFor("application/javascript"),
|
||||||
|
},
|
||||||
|
|
||||||
|
woff2: {
|
||||||
|
resultMapper: resultMapperFor("font/woff2"),
|
||||||
|
},
|
||||||
|
|
||||||
|
ttf: {
|
||||||
|
resultMapper: resultMapperFor("font/ttf"),
|
||||||
|
},
|
||||||
|
};
|
||||||
249
src/main/router.test.ts
Normal file
249
src/main/router.test.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import routerInjectable, { routeInjectionToken } from "./router/router.injectable";
|
||||||
|
import { getDiForUnitTesting } from "./getDiForUnitTesting";
|
||||||
|
import type { Router, RouteHandler, Route } 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 "./router/parse-request.injectable";
|
||||||
|
import { contentTypes } from "./router-content-types";
|
||||||
|
|
||||||
|
describe("router", () => {
|
||||||
|
let router: Router;
|
||||||
|
let routeHandlerMock: AsyncFnMock<RouteHandler<any>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
routeHandlerMock = asyncFn();
|
||||||
|
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.override(parseRequestInjectable, () => () => Promise.resolve({ payload: "some-payload" }));
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
di.register(getInjectable({
|
||||||
|
id: "some-route",
|
||||||
|
|
||||||
|
instantiate: (): Route<any> => ({
|
||||||
|
method: "get",
|
||||||
|
path: "/some-path",
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
handler: routeHandlerMock,
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: routeInjectionToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
router = di.inject(routerInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when navigating to the route", () => {
|
||||||
|
let actualPromise: Promise<boolean>;
|
||||||
|
let clusterStub: Cluster;
|
||||||
|
let requestStub: Request;
|
||||||
|
let responseStub: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
requestStub = new Request({
|
||||||
|
url: "/some-path",
|
||||||
|
method: "get",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
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 },
|
||||||
|
response: 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"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ 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).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -4,12 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Call from "@hapi/call";
|
import Call from "@hapi/call";
|
||||||
import Subtext from "@hapi/subtext";
|
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import type httpProxy from "http-proxy";
|
import type httpProxy from "http-proxy";
|
||||||
import { toPairs } from "lodash/fp";
|
import { toPairs } from "lodash/fp";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import type { Cluster } from "../common/cluster/cluster";
|
import type { Cluster } from "../common/cluster/cluster";
|
||||||
|
import type { LensApiResultContentType } from "./router-content-types";
|
||||||
|
import { contentTypes } from "./router-content-types";
|
||||||
|
import "../common/vars";
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
@ -42,138 +44,17 @@ export interface LensApiRequest<P = any> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const respondFor =
|
interface Dependencies {
|
||||||
(contentType: string) =>
|
parseRequest: (request: http.IncomingMessage, _: null, options: { parse: boolean; output: string }) => Promise<{ payload: any }>;
|
||||||
(
|
}
|
||||||
content: any,
|
|
||||||
statusCode: number,
|
|
||||||
response: http.ServerResponse,
|
|
||||||
) => {
|
|
||||||
response.statusCode = statusCode;
|
|
||||||
response.setHeader("Content-Type", contentType);
|
|
||||||
|
|
||||||
if (content instanceof Buffer) {
|
|
||||||
response.write(content);
|
|
||||||
|
|
||||||
response.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
response.end(content);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SupportedFileExtension = "json" | "txt" | "html" | "css" | "gif" | "jpg" | "png" | "svg" | "js" | "woff2" | "ttf";
|
|
||||||
|
|
||||||
export const contentTypes: Record<SupportedFileExtension, LensApiResultContentType> = {
|
|
||||||
json: {
|
|
||||||
respond: (
|
|
||||||
content: any,
|
|
||||||
statusCode: number,
|
|
||||||
response: http.ServerResponse,
|
|
||||||
) => {
|
|
||||||
response.statusCode = statusCode;
|
|
||||||
response.setHeader("Content-Type", "application/json");
|
|
||||||
|
|
||||||
if (content instanceof Buffer) {
|
|
||||||
response.write(content);
|
|
||||||
response.end();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedContent =
|
|
||||||
typeof content === "object" ? JSON.stringify(content) : content;
|
|
||||||
|
|
||||||
response.end(normalizedContent);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
txt: {
|
|
||||||
respond: respondFor("text/plain"),
|
|
||||||
},
|
|
||||||
|
|
||||||
html: {
|
|
||||||
respond: respondFor("text/html"),
|
|
||||||
},
|
|
||||||
|
|
||||||
css: {
|
|
||||||
respond: respondFor("text/css"),
|
|
||||||
},
|
|
||||||
|
|
||||||
gif: {
|
|
||||||
respond: respondFor("image/gif"),
|
|
||||||
},
|
|
||||||
|
|
||||||
jpg: {
|
|
||||||
respond: respondFor("image/jpeg"),
|
|
||||||
},
|
|
||||||
|
|
||||||
png: {
|
|
||||||
respond: respondFor("image/png"),
|
|
||||||
},
|
|
||||||
|
|
||||||
svg: {
|
|
||||||
respond: respondFor("image/svg+xml"),
|
|
||||||
},
|
|
||||||
|
|
||||||
js: {
|
|
||||||
respond: respondFor("application/javascript"),
|
|
||||||
},
|
|
||||||
|
|
||||||
woff2: {
|
|
||||||
respond: respondFor("font/woff2"),
|
|
||||||
},
|
|
||||||
|
|
||||||
ttf: {
|
|
||||||
respond: respondFor("font/ttf"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
protected router = new Call.Router();
|
protected router = new Call.Router();
|
||||||
protected static rootPath = path.resolve(__static);
|
protected static rootPath = path.resolve(__static);
|
||||||
|
|
||||||
constructor(routes: Route<any>[]) {
|
constructor(routes: Route<any>[], private dependencies: Dependencies) {
|
||||||
routes.forEach(route => {
|
routes.forEach(route => {
|
||||||
this.router.add({ method: route.method, path: route.path }, async (request: LensApiRequest) => {
|
this.router.add({ method: route.method, path: route.path }, handleRoute(route));
|
||||||
let result: LensApiResult<any> | void;
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await route.handler(request);
|
|
||||||
} catch(error) {
|
|
||||||
contentTypes.txt.respond(error.toString(), 422, request.response);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
contentTypes.txt.respond(null, 204, request.response);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
response,
|
|
||||||
error,
|
|
||||||
statusCode = error ? 400 : 200,
|
|
||||||
contentType = contentTypes.json,
|
|
||||||
headers = {},
|
|
||||||
proxy,
|
|
||||||
} = result;
|
|
||||||
|
|
||||||
if (proxy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerNameValuePairs = toPairs<string>(headers);
|
|
||||||
|
|
||||||
headerNameValuePairs.forEach(([key, value]) => {
|
|
||||||
request.response.setHeader(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
contentType.respond(error || response, statusCode, request.response);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +78,8 @@ export class Router {
|
|||||||
|
|
||||||
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest> {
|
protected async getRequest(opts: RouterRequestOpts): Promise<LensApiRequest> {
|
||||||
const { req, res, url, cluster, params } = opts;
|
const { req, res, url, cluster, params } = opts;
|
||||||
const { payload } = await Subtext.parse(req, null, {
|
|
||||||
|
const { payload } = await this.dependencies.parseRequest(req, null, {
|
||||||
parse: true,
|
parse: true,
|
||||||
output: "data",
|
output: "data",
|
||||||
});
|
});
|
||||||
@ -216,27 +98,95 @@ export class Router {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LensApiResultContentType {
|
|
||||||
respond: (content: any, statusCode: number, response: http.ServerResponse) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LensApiResult<TResult> {
|
export interface LensApiResult<TResult> {
|
||||||
statusCode?: number;
|
statusCode?: number;
|
||||||
response?: TResult;
|
response?: TResult;
|
||||||
error?: any;
|
error?: any;
|
||||||
contentType?: LensApiResultContentType;
|
contentType?: LensApiResultContentType;
|
||||||
headers?: { Location: string };
|
headers?: { [name: string]: string };
|
||||||
proxy?: httpProxy;
|
proxy?: httpProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RouteHandler<TResponse> = (
|
||||||
|
request: LensApiRequest
|
||||||
|
) =>
|
||||||
|
| Promise<LensApiResult<TResponse>>
|
||||||
|
| Promise<void>
|
||||||
|
| LensApiResult<TResponse>
|
||||||
|
| void;
|
||||||
|
|
||||||
export interface Route<TResponse> {
|
export interface Route<TResponse> {
|
||||||
path: string;
|
path: string;
|
||||||
method: "get" | "post" | "put" | "patch" | "delete";
|
method: "get" | "post" | "put" | "patch" | "delete";
|
||||||
handler: (
|
handler: RouteHandler<TResponse>;
|
||||||
request: LensApiRequest
|
|
||||||
) =>
|
|
||||||
| Promise<LensApiResult<TResponse>>
|
|
||||||
| Promise<void>
|
|
||||||
| LensApiResult<TResponse>
|
|
||||||
| void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleRoute = (route: Route<any>) => async (request: LensApiRequest) => {
|
||||||
|
let result: LensApiResult<any> | void;
|
||||||
|
|
||||||
|
const writeServerResponse = writeServerResponseFor(request.response);
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await route.handler(request);
|
||||||
|
} catch(error) {
|
||||||
|
const mappedResult = contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 422,
|
||||||
|
error: error.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
const mappedResult = contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 204,
|
||||||
|
response: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.proxy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = result.contentType || contentTypes.json;
|
||||||
|
|
||||||
|
const mappedResult = contentType.resultMapper(result);
|
||||||
|
|
||||||
|
writeServerResponse(mappedResult);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeServerResponseFor =
|
||||||
|
(serverResponse: http.ServerResponse) =>
|
||||||
|
({
|
||||||
|
statusCode,
|
||||||
|
content,
|
||||||
|
headers,
|
||||||
|
}: {
|
||||||
|
statusCode: number;
|
||||||
|
content: any;
|
||||||
|
headers: { [name: string]: string };
|
||||||
|
}) => {
|
||||||
|
serverResponse.statusCode = statusCode;
|
||||||
|
|
||||||
|
const headerPairs = toPairs<string>(headers);
|
||||||
|
|
||||||
|
headerPairs.forEach(([name, value]) => {
|
||||||
|
serverResponse.setHeader(name, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (content instanceof Buffer) {
|
||||||
|
serverResponse.write(content);
|
||||||
|
|
||||||
|
serverResponse.end();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serverResponse.end(content);
|
||||||
|
};
|
||||||
|
|||||||
13
src/main/router/parse-request.injectable.ts
Normal file
13
src/main/router/parse-request.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
const parseRequestInjectable = getInjectable({
|
||||||
|
id: "parse-http-request",
|
||||||
|
instantiate: () => Subtext.parse,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default parseRequestInjectable;
|
||||||
@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import { Route, Router } from "../router";
|
import { Route, Router } from "../router";
|
||||||
|
import parseRequestInjectable from "./parse-request.injectable";
|
||||||
|
|
||||||
export const routeInjectionToken = getInjectionToken<Route<any>>({
|
export const routeInjectionToken = getInjectionToken<Route<any>>({
|
||||||
id: "route-injection-token",
|
id: "route-injection-token",
|
||||||
@ -15,7 +16,9 @@ const routerInjectable = getInjectable({
|
|||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const routes = di.injectMany(routeInjectionToken);
|
const routes = di.injectMany(routeInjectionToken);
|
||||||
|
|
||||||
return new Router(routes);
|
return new Router(routes, {
|
||||||
|
parseRequest: di.inject(parseRequestInjectable),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { helmService } from "../../../helm/helm-service";
|
|||||||
import { routeInjectionToken } from "../../../router/router.injectable";
|
import { routeInjectionToken } from "../../../router/router.injectable";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { getBoolean } from "../../../utils/parse-query";
|
import { getBoolean } from "../../../utils/parse-query";
|
||||||
import { contentTypes } from "../../../router";
|
import { contentTypes } from "../../../router-content-types";
|
||||||
|
|
||||||
const getReleaseRouteValuesInjectable = getInjectable({
|
const getReleaseRouteValuesInjectable = getInjectable({
|
||||||
id: "get-release-values-route",
|
id: "get-release-values-route",
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { LensApiRequest, SupportedFileExtension, Route } from "../router";
|
import type { LensApiRequest, Route } from "../router";
|
||||||
import { contentTypes } from "../router";
|
import { contentTypes, SupportedFileExtension } from "../router-content-types";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { routeInjectionToken } from "../router/router.injectable";
|
import { routeInjectionToken } from "../router/router.injectable";
|
||||||
import { appName, publicPath } from "../../common/vars";
|
import { appName, publicPath } from "../../common/vars";
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@ -9047,6 +9047,11 @@ merge2@^1.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||||
|
|
||||||
|
mergee@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mergee/-/mergee-1.0.0.tgz#027c5addc650f6ecbe4bf56100bd00dae763fda7"
|
||||||
|
integrity sha512-hbbXD4LOcxVkpS+mp3BMEhkSDf+lTVENFeEeqACgjjL8WrgKuW2EyLT0fOHyTbyDiuRLZJZ1HrHNeiX4iOd79Q==
|
||||||
|
|
||||||
methods@~1.1.2:
|
methods@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
@ -9290,6 +9295,13 @@ mock-fs@^5.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7"
|
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7"
|
||||||
integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==
|
integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==
|
||||||
|
|
||||||
|
mock-http@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mock-http/-/mock-http-1.1.0.tgz#b89380a718a103fc5801095804bedd0b20f7638c"
|
||||||
|
integrity sha512-H2HMGaHNQPWY8PdeEw4RFux2WEOHD6eJAtN3+iFELik5kGjPKAcoyPWcsC2vgDiTa2yimAEDssmMed51e+cBKQ==
|
||||||
|
dependencies:
|
||||||
|
mergee "^1.0.0"
|
||||||
|
|
||||||
moment-timezone@^0.5.34:
|
moment-timezone@^0.5.34:
|
||||||
version "0.5.34"
|
version "0.5.34"
|
||||||
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
|
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user