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:
parent
086db9fce7
commit
711516cccf
@ -3,7 +3,9 @@
|
|||||||
* 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 { Router } from "../router";
|
import type { LensApiRequest } from "../router";
|
||||||
|
import staticFileRouteInjectable from "../routes/static-file-route.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
@ -22,19 +24,31 @@ jest.mock("electron", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe("Router", () => {
|
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 () => {
|
it("blocks path traversal attacks", async () => {
|
||||||
const response: any = {
|
const response: any = {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
end: jest.fn(),
|
end: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await (Router as any).handleStaticFile({
|
const request = {
|
||||||
params: {
|
params: {
|
||||||
path: "../index.ts",
|
path: "../index.ts",
|
||||||
},
|
},
|
||||||
response,
|
response,
|
||||||
raw: {},
|
raw: {},
|
||||||
});
|
} as LensApiRequest<any>;
|
||||||
|
|
||||||
|
await handleStaticFile(request);
|
||||||
|
|
||||||
expect(response.statusCode).toEqual(404);
|
expect(response.statusCode).toEqual(404);
|
||||||
});
|
});
|
||||||
@ -46,17 +60,20 @@ describe("Router", () => {
|
|||||||
setHeader: jest.fn(),
|
setHeader: jest.fn(),
|
||||||
end: jest.fn(),
|
end: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const req: any = {
|
const req: any = {
|
||||||
url: "",
|
url: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
await (Router as any).handleStaticFile({
|
const request = {
|
||||||
params: {
|
params: {
|
||||||
path: "router.test.ts",
|
path: "router.test.ts",
|
||||||
},
|
},
|
||||||
response,
|
response,
|
||||||
raw: { req },
|
raw: { req },
|
||||||
});
|
} as LensApiRequest<any>;
|
||||||
|
|
||||||
|
await handleStaticFile(request);
|
||||||
|
|
||||||
expect(response.statusCode).toEqual(200);
|
expect(response.statusCode).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,12 +6,8 @@
|
|||||||
import Call from "@hapi/call";
|
import Call from "@hapi/call";
|
||||||
import Subtext from "@hapi/subtext";
|
import Subtext from "@hapi/subtext";
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
import type httpProxy from "http-proxy";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { readFile } from "fs-extra";
|
|
||||||
import type { Cluster } from "../common/cluster/cluster";
|
import type { Cluster } from "../common/cluster/cluster";
|
||||||
import { appName, publicPath } from "../common/vars";
|
|
||||||
import logger from "./logger";
|
|
||||||
|
|
||||||
export interface RouterRequestOpts {
|
export interface RouterRequestOpts {
|
||||||
req: http.IncomingMessage;
|
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 {
|
export class Router {
|
||||||
protected router = new Call.Router();
|
protected router = new Call.Router();
|
||||||
protected static rootPath = path.resolve(__static);
|
static rootPath = path.resolve(__static);
|
||||||
|
|
||||||
public constructor(private dependencies: Dependencies) {
|
|
||||||
this.addRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
|
||||||
const url = new URL(req.url, "http://localhost");
|
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) => {
|
addRoute = (route: Route) => {
|
||||||
this.router.add({ method: route.method, path: route.path }, (request: LensApiRequest) => {
|
this.router.add({ method: route.method, path: route.path }, (request: LensApiRequest) => {
|
||||||
route.handler(request);
|
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 {
|
export interface Route {
|
||||||
path: string;
|
path: string;
|
||||||
method: "get" | "post" | "put" | "patch" | "delete";
|
method: "get" | "post" | "put" | "patch" | "delete";
|
||||||
handler: (request: LensApiRequest) => Promise<void>;
|
handler: (request: LensApiRequest) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import { Router, Route } from "../router";
|
import { Router, Route } from "../router";
|
||||||
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
|
|
||||||
import httpProxy from "http-proxy";
|
|
||||||
|
|
||||||
export const routeInjectionToken = getInjectionToken<Route>({
|
export const routeInjectionToken = getInjectionToken<Route>({
|
||||||
id: "route-injection-token",
|
id: "route-injection-token",
|
||||||
@ -15,12 +13,7 @@ const routerInjectable = getInjectable({
|
|||||||
id: "router",
|
id: "router",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const isDevelopment = di.inject(isDevelopmentInjectable);
|
const router = new Router();
|
||||||
const proxy = isDevelopment ? httpProxy.createProxy() : undefined;
|
|
||||||
|
|
||||||
const router = new Router({
|
|
||||||
httpProxy: proxy,
|
|
||||||
});
|
|
||||||
|
|
||||||
const routes = di.injectMany(routeInjectionToken);
|
const routes = di.injectMany(routeInjectionToken);
|
||||||
|
|
||||||
|
|||||||
110
src/main/routes/static-file-route.injectable.ts
Normal file
110
src/main/routes/static-file-route.injectable.ts
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user