1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Switch to using injectable to setup route for serving static files

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-02-11 12:45:12 +02:00
parent 086db9fce7
commit 711516cccf
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
4 changed files with 135 additions and 95 deletions

View File

@ -3,7 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Router } from "../router";
import type { LensApiRequest } from "../router";
import staticFileRouteInjectable from "../routes/static-file-route.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
jest.mock("electron", () => ({
app: {
@ -22,19 +24,31 @@ jest.mock("electron", () => ({
}));
describe("Router", () => {
let handleStaticFile: (request: LensApiRequest) => Promise<void>;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups();
handleStaticFile = di.inject(staticFileRouteInjectable).handler;
});
it("blocks path traversal attacks", async () => {
const response: any = {
statusCode: 200,
end: jest.fn(),
};
await (Router as any).handleStaticFile({
const request = {
params: {
path: "../index.ts",
},
response,
raw: {},
});
} as LensApiRequest<any>;
await handleStaticFile(request);
expect(response.statusCode).toEqual(404);
});
@ -46,17 +60,20 @@ describe("Router", () => {
setHeader: jest.fn(),
end: jest.fn(),
};
const req: any = {
url: "",
};
await (Router as any).handleStaticFile({
const request = {
params: {
path: "router.test.ts",
},
response,
raw: { req },
});
} as LensApiRequest<any>;
await handleStaticFile(request);
expect(response.statusCode).toEqual(200);
});

View File

@ -6,12 +6,8 @@
import Call from "@hapi/call";
import Subtext from "@hapi/subtext";
import type http from "http";
import type httpProxy from "http-proxy";
import path from "path";
import { readFile } from "fs-extra";
import type { Cluster } from "../common/cluster/cluster";
import { appName, publicPath } from "../common/vars";
import logger from "./logger";
export interface RouterRequestOpts {
req: http.IncomingMessage;
@ -44,34 +40,9 @@ export interface LensApiRequest<P = any> {
};
}
function getMimeType(filename: string) {
const mimeTypes: Record<string, string> = {
html: "text/html",
txt: "text/plain",
css: "text/css",
gif: "image/gif",
jpg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
js: "application/javascript",
woff2: "font/woff2",
ttf: "font/ttf",
};
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
interface Dependencies {
httpProxy?: httpProxy;
}
export class Router {
protected router = new Call.Router();
protected static rootPath = path.resolve(__static);
public constructor(private dependencies: Dependencies) {
this.addRoutes();
}
static rootPath = path.resolve(__static);
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const url = new URL(req.url, "http://localhost");
@ -111,66 +82,15 @@ export class Router {
};
}
protected static async handleStaticFile({ params, response }: LensApiRequest): Promise<void> {
let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
const asset = path.join(Router.rootPath, filePath);
const normalizedFilePath = path.resolve(asset);
if (!normalizedFilePath.startsWith(Router.rootPath)) {
response.statusCode = 404;
return response.end();
}
try {
const data = await readFile(asset);
response.setHeader("Content-Type", getMimeType(asset));
response.write(data);
response.end();
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString());
response.statusCode = 404;
return response.end();
}
filePath = `${publicPath}/${appName}.html`;
}
}
}
addRoute = (route: Route) => {
this.router.add({ method: route.method, path: route.path }, (request: LensApiRequest) => {
route.handler(request);
});
};
protected addRoutes() {
// Static assets
if (this.dependencies.httpProxy) {
this.router.add({ method: "get", path: "/{path*}" }, (apiReq: LensApiRequest) => {
const { req, res } = apiReq.raw;
if (req.url === "/" || !req.url.startsWith("/build/")) {
req.url = `${publicPath}/${appName}.html`;
}
this.dependencies.httpProxy.web(req, res, {
target: "http://127.0.0.1:8080",
});
});
} else {
this.router.add({ method: "get", path: "/{path*}" }, Router.handleStaticFile);
}
}
}
export interface Route {
path: string;
method: "get" | "post" | "put" | "patch" | "delete";
handler: (request: LensApiRequest) => Promise<void>;
handler: (request: LensApiRequest) => void | Promise<void>;
}

View File

@ -4,8 +4,6 @@
*/
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { Router, Route } from "../router";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import httpProxy from "http-proxy";
export const routeInjectionToken = getInjectionToken<Route>({
id: "route-injection-token",
@ -15,12 +13,7 @@ const routerInjectable = getInjectable({
id: "router",
instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable);
const proxy = isDevelopment ? httpProxy.createProxy() : undefined;
const router = new Router({
httpProxy: proxy,
});
const router = new Router();
const routes = di.injectMany(routeInjectionToken);

View File

@ -0,0 +1,110 @@
/**
* 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 type { LensApiRequest, Route } from "../router";
import logger from "../logger";
import { routeInjectionToken } from "../router/router.injectable";
import {
appName,
publicPath,
} from "../../common/vars";
import path from "path";
import readFileInjectable from "../../common/fs/read-file.injectable";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import httpProxy from "http-proxy";
import { Router } from "../router";
function getMimeType(filename: string) {
const mimeTypes: Record<string, string> = {
html: "text/html",
txt: "text/plain",
css: "text/css",
gif: "image/gif",
jpg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
js: "application/javascript",
woff2: "font/woff2",
ttf: "font/ttf",
};
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
interface ProductionDependencies {
readFile: (path: string) => Promise<Buffer>;
}
const handleStaticFileInProduction =
({ readFile }: ProductionDependencies) =>
async ({ params, response }: LensApiRequest): Promise<void> => {
let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
const asset = path.join(Router.rootPath, filePath);
const normalizedFilePath = path.resolve(asset);
if (!normalizedFilePath.startsWith(Router.rootPath)) {
response.statusCode = 404;
return response.end();
}
try {
const data = await readFile(asset);
response.setHeader("Content-Type", getMimeType(asset));
response.write(data);
response.end();
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString());
response.statusCode = 404;
return response.end();
}
filePath = `${publicPath}/${appName}.html`;
}
}
};
interface DevelopmentDependencies {
proxy: httpProxy;
}
const handleStaticFileInDevelopment =
({ proxy }: DevelopmentDependencies) =>
(apiReq: LensApiRequest) => {
const { req, res } = apiReq.raw;
if (req.url === "/" || !req.url.startsWith("/build/")) {
req.url = `${publicPath}/${appName}.html`;
}
proxy.web(req, res, {
target: "http://127.0.0.1:8080",
});
};
const staticFileRouteInjectable = getInjectable({
id: "static-file-route",
instantiate: (di): Route => {
const isDevelopment = di.inject(isDevelopmentInjectable);
return {
method: "get",
path: `/{path*}`,
handler: isDevelopment
? handleStaticFileInDevelopment({ proxy: httpProxy.createProxy() })
: handleStaticFileInProduction({ readFile: di.inject(readFileInjectable) }),
};
},
injectionToken: routeInjectionToken,
});
export default staticFileRouteInjectable;