mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introuduce auth header value and retrieval on renderer
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
6781b4657f
commit
a5dab8549b
11
src/common/auth/channel.ts
Normal file
11
src/common/auth/channel.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
|
||||||
|
|
||||||
|
// This channel retreives the value needed to grant authentication to requests
|
||||||
|
export const lensAuthenticationChannel: RequestChannel<void, string> = {
|
||||||
|
id: "lens-authentication-channel",
|
||||||
|
};
|
||||||
9
src/common/vars/auth-header.ts
Normal file
9
src/common/vars/auth-header.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the header name that we use for request authentication
|
||||||
|
*/
|
||||||
|
export const lensAuthenticationHeader = "LENS-AUTHENTICATION";
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { lensAuthenticationChannel } from "../../common/auth/channel";
|
||||||
|
import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens";
|
||||||
|
import authHeaderValueInjectable from "./auth-header-value.injectable";
|
||||||
|
|
||||||
|
const lensAuthenticationRequestListener = getRequestChannelListenerInjectable({
|
||||||
|
channel: lensAuthenticationChannel,
|
||||||
|
handler: (di) => {
|
||||||
|
const authHeaderValue = di.inject(authHeaderValueInjectable);
|
||||||
|
|
||||||
|
return () => authHeaderValue;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default lensAuthenticationRequestListener;
|
||||||
13
src/main/lens-proxy/auth-header-value.injectable.ts
Normal file
13
src/main/lens-proxy/auth-header-value.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 * as uuid from "uuid";
|
||||||
|
|
||||||
|
const authHeaderValueInjectable = getInjectable({
|
||||||
|
id: "auth-header-value",
|
||||||
|
instantiate: () => uuid.v4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authHeaderValueInjectable;
|
||||||
@ -5,7 +5,7 @@
|
|||||||
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 routeRequestInjectable from "../router/router.injectable";
|
||||||
import httpProxy from "http-proxy";
|
import httpProxy from "http-proxy";
|
||||||
import clusterManagerInjectable from "../cluster/manager.injectable";
|
import clusterManagerInjectable from "../cluster/manager.injectable";
|
||||||
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
||||||
@ -13,12 +13,13 @@ import lensProxyPortInjectable from "./lens-proxy-port.injectable";
|
|||||||
import contentSecurityPolicyInjectable from "../../common/vars/content-security-policy.injectable";
|
import contentSecurityPolicyInjectable from "../../common/vars/content-security-policy.injectable";
|
||||||
import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable";
|
import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable";
|
||||||
import loggerInjectable from "../../common/logger.injectable";
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
|
import authHeaderValueInjectable from "./auth-header-value.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),
|
||||||
@ -27,6 +28,7 @@ const lensProxyInjectable = getInjectable({
|
|||||||
contentSecurityPolicy: di.inject(contentSecurityPolicyInjectable),
|
contentSecurityPolicy: di.inject(contentSecurityPolicyInjectable),
|
||||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||||
logger: di.inject(loggerInjectable),
|
logger: di.inject(loggerInjectable),
|
||||||
|
authHeaderValue: di.inject(authHeaderValueInjectable),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import net from "net";
|
|||||||
import http from "http";
|
import 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";
|
||||||
@ -16,6 +15,10 @@ import assert from "assert";
|
|||||||
import type { SetRequired } from "type-fest";
|
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 { RouteRequest } from "../router/router.injectable";
|
||||||
|
import { lensAuthenticationHeader } from "../../common/vars/auth-header";
|
||||||
|
import { contentTypes } from "../router/router-content-types";
|
||||||
|
import { writeServerResponseFor } from "../router/write-server-response";
|
||||||
|
|
||||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
|
||||||
|
|
||||||
@ -26,11 +29,12 @@ interface Dependencies {
|
|||||||
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||||
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||||
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;
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
|
readonly authHeaderValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchParam = "watch";
|
const watchParam = "watch";
|
||||||
@ -61,9 +65,9 @@ const disallowedPorts = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export class LensProxy {
|
export class LensProxy {
|
||||||
protected proxyServer: http.Server;
|
protected readonly proxyServer: http.Server;
|
||||||
protected closed = false;
|
protected closed = false;
|
||||||
protected retryCounters = new Map<string, number>();
|
protected readonly retryCounters = new Map<string, number>();
|
||||||
|
|
||||||
constructor(private readonly dependencies: Dependencies) {
|
constructor(private readonly dependencies: Dependencies) {
|
||||||
this.configureProxy(dependencies.proxy);
|
this.configureProxy(dependencies.proxy);
|
||||||
@ -75,6 +79,13 @@ export class LensProxy {
|
|||||||
this.proxyServer
|
this.proxyServer
|
||||||
.on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => {
|
.on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||||
const cluster = dependencies.getClusterForRequest(req);
|
const cluster = dependencies.getClusterForRequest(req);
|
||||||
|
const authHeader = req.headers[lensAuthenticationHeader.toLowerCase()];
|
||||||
|
|
||||||
|
if (authHeader !== this.dependencies.authHeaderValue) {
|
||||||
|
socket.destroy(new Error("Missing authorization"));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
this.dependencies.logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
this.dependencies.logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
||||||
@ -210,13 +221,15 @@ export class LensProxy {
|
|||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise<httpProxy.ServerOptions | void> {
|
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise<httpProxy.ServerOptions | undefined> {
|
||||||
if (req.url?.startsWith(apiKubePrefix)) {
|
if (req.url?.startsWith(apiKubePrefix)) {
|
||||||
delete req.headers.authorization;
|
delete req.headers.authorization;
|
||||||
req.url = req.url.replace(apiKubePrefix, "");
|
req.url = req.url.replace(apiKubePrefix, "");
|
||||||
|
|
||||||
return contextHandler.getApiTarget(isLongRunningRequest(req.url));
|
return contextHandler.getApiTarget(isLongRunningRequest(req.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getRequestId(req: http.IncomingMessage): string {
|
protected getRequestId(req: http.IncomingMessage): string {
|
||||||
@ -227,16 +240,28 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
|
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
|
||||||
const cluster = this.dependencies.getClusterForRequest(req);
|
const cluster = this.dependencies.getClusterForRequest(req);
|
||||||
|
const writeServerResponse = writeServerResponseFor(res);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||||
|
|
||||||
if (proxyTarget) {
|
if (proxyTarget) {
|
||||||
|
const authHeader = req.headers[lensAuthenticationHeader.toLowerCase()];
|
||||||
|
|
||||||
|
if (authHeader !== this.dependencies.authHeaderValue) {
|
||||||
|
writeServerResponse(contentTypes.txt.resultMapper({
|
||||||
|
statusCode: 401,
|
||||||
|
response: "Missing authorization",
|
||||||
|
}));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return this.dependencies.proxy.web(req, res, proxyTarget);
|
return this.dependencies.proxy.web(req, res, proxyTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = 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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
@ -65,6 +66,7 @@ export interface RouteHandler<TResponse, Path extends string>{
|
|||||||
export interface BaseRoutePaths<Path extends string> {
|
export interface BaseRoutePaths<Path extends string> {
|
||||||
path: Path;
|
path: Path;
|
||||||
method: "get" | "post" | "put" | "patch" | "delete";
|
method: "get" | "post" | "put" | "patch" | "delete";
|
||||||
|
requireAuthentication?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayloadValidator<Payload> {
|
export interface PayloadValidator<Payload> {
|
||||||
@ -77,15 +79,20 @@ export interface ValidatorBaseRoutePaths<Path extends string, Payload> extends B
|
|||||||
|
|
||||||
export interface Route<TResponse, Path extends string> extends BaseRoutePaths<Path> {
|
export interface Route<TResponse, Path extends string> extends BaseRoutePaths<Path> {
|
||||||
handler: RouteHandler<TResponse, Path>;
|
handler: RouteHandler<TResponse, Path>;
|
||||||
|
requireAuthentication: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BindHandler<Path extends string> {
|
export interface BindHandler<Path extends string> {
|
||||||
<TResponse>(handler: RouteHandler<TResponse, Path>): Route<TResponse, Path>;
|
<TResponse>(handler: RouteHandler<TResponse, Path>): Route<TResponse, Path>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function route<Path extends string>(parts: BaseRoutePaths<Path>): BindHandler<Path> {
|
export function route<Path extends string>({
|
||||||
|
requireAuthentication = true,
|
||||||
|
...parts
|
||||||
|
}: BaseRoutePaths<Path>): BindHandler<Path> {
|
||||||
return (handler) => ({
|
return (handler) => ({
|
||||||
...parts,
|
...parts,
|
||||||
|
requireAuthentication,
|
||||||
handler,
|
handler,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -98,8 +105,12 @@ export interface BindClusterHandler<Path extends string> {
|
|||||||
<TResponse>(handler: ClusterRouteHandler<TResponse, Path>): Route<TResponse, Path>;
|
<TResponse>(handler: ClusterRouteHandler<TResponse, Path>): Route<TResponse, Path>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clusterRoute<Path extends string>(parts: BaseRoutePaths<Path>): BindClusterHandler<Path> {
|
export function clusterRoute<Path extends string>({
|
||||||
|
requireAuthentication = true,
|
||||||
|
...parts
|
||||||
|
}: BaseRoutePaths<Path>): BindClusterHandler<Path> {
|
||||||
return (handler) => ({
|
return (handler) => ({
|
||||||
|
requireAuthentication,
|
||||||
...parts,
|
...parts,
|
||||||
handler: ({ cluster, ...rest }) => {
|
handler: ({ cluster, ...rest }) => {
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
|
|||||||
@ -2,12 +2,16 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* 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 type { Injectable, InjectionToken } from "@ogre-tools/injectable";
|
import type { DiContainerForInjection, Injectable, InjectionToken } from "@ogre-tools/injectable";
|
||||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||||
import { Router } from "./router";
|
|
||||||
import parseRequestInjectable from "./parse-request.injectable";
|
import parseRequestInjectable from "./parse-request.injectable";
|
||||||
import type { Route } from "./route";
|
import type { Route, LensApiRequest } from "./route";
|
||||||
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
|
import createHandlerForRouteInjectable from "./create-handler-for-route.injectable";
|
||||||
|
import Call from "@hapi/call";
|
||||||
|
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>>({
|
export const routeInjectionToken = getInjectionToken<Route<unknown, string>>({
|
||||||
id: "route-injection-token",
|
id: "route-injection-token",
|
||||||
@ -22,14 +26,75 @@ export function getRouteInjectable<T, Path extends string>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const routerInjectable = getInjectable({
|
export type RouteRequest = (cluster: Cluster | undefined, req: ServerIncomingMessage, res: http.ServerResponse) => Promise<boolean>;
|
||||||
id: "router",
|
|
||||||
|
|
||||||
instantiate: (di) => new Router({
|
const createRouter = (di: DiContainerForInjection) => {
|
||||||
parseRequest: di.inject(parseRequestInjectable),
|
const routes = di.injectMany(routeInjectionToken);
|
||||||
routes: di.injectMany(routeInjectionToken),
|
const createHandlerForRoute = di.inject(createHandlerForRouteInjectable);
|
||||||
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RouterRequestOpts {
|
||||||
|
req: http.IncomingMessage;
|
||||||
|
res: http.ServerResponse;
|
||||||
|
cluster: Cluster | undefined;
|
||||||
|
params: Partial<Record<string, string>>;
|
||||||
|
url: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRequestWith = (di: DiContainerForInjection) => {
|
||||||
|
const parseRequest = di.inject(parseRequestInjectable);
|
||||||
|
|
||||||
|
return async (opts: RouterRequestOpts): Promise<LensApiRequest<string>> => {
|
||||||
|
const { req, res, url, cluster, params } = opts;
|
||||||
|
const { payload } = await parseRequest(req, null, {
|
||||||
|
parse: true,
|
||||||
|
output: "data",
|
||||||
});
|
});
|
||||||
|
|
||||||
export default routerInjectable;
|
return {
|
||||||
|
cluster,
|
||||||
|
path: url.pathname,
|
||||||
|
raw: {
|
||||||
|
req, res,
|
||||||
|
},
|
||||||
|
query: url.searchParams,
|
||||||
|
payload,
|
||||||
|
params,
|
||||||
|
getHeader: (key) => req.headers[key.toLowerCase()],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeRequestInjectable = getInjectable({
|
||||||
|
id: "route-request",
|
||||||
|
instantiate: (di): RouteRequest => {
|
||||||
|
const router = createRouter(di);
|
||||||
|
const getRequest = getRequestWith(di);
|
||||||
|
|
||||||
|
return async (cluster, req, res) => {
|
||||||
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
const path = url.pathname;
|
||||||
|
const method = req.method.toLowerCase();
|
||||||
|
const matchingRoute = router.route(method, path);
|
||||||
|
|
||||||
|
if (matchingRoute instanceof Error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = await getRequest({ req, res, cluster, url, params: matchingRoute.params });
|
||||||
|
|
||||||
|
await matchingRoute.route(request, res);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default routeRequestInjectable;
|
||||||
|
|||||||
@ -3,9 +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 routerInjectable, { routeInjectionToken } from "./router.injectable";
|
import type { RouteRequest } from "./router.injectable";
|
||||||
|
import routeRequestInjectable, { routeInjectionToken } from "./router.injectable";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import type { Router } from "./router";
|
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import { Request } from "mock-http";
|
import { Request } from "mock-http";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
@ -24,7 +24,7 @@ import fsInjectable from "../../common/fs/fs.injectable";
|
|||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
describe("router", () => {
|
describe("router", () => {
|
||||||
let router: Router;
|
let routeRequest: RouteRequest;
|
||||||
let routeHandlerMock: AsyncFnMock<() => any>;
|
let routeHandlerMock: AsyncFnMock<() => any>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -51,6 +51,7 @@ describe("router", () => {
|
|||||||
method: "get",
|
method: "get",
|
||||||
path: "/some-path",
|
path: "/some-path",
|
||||||
handler: routeHandlerMock,
|
handler: routeHandlerMock,
|
||||||
|
requireAuthentication: false,
|
||||||
} as Route<any, string>),
|
} as Route<any, string>),
|
||||||
|
|
||||||
injectionToken: routeInjectionToken,
|
injectionToken: routeInjectionToken,
|
||||||
@ -60,7 +61,7 @@ describe("router", () => {
|
|||||||
di.register(injectable);
|
di.register(injectable);
|
||||||
});
|
});
|
||||||
|
|
||||||
router = di.inject(routerInjectable);
|
routeRequest = di.inject(routeRequestInjectable);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -86,7 +87,7 @@ describe("router", () => {
|
|||||||
|
|
||||||
clusterStub = {} as Cluster;
|
clusterStub = {} as Cluster;
|
||||||
|
|
||||||
actualPromise = router.route(clusterStub, requestStub, responseStub);
|
actualPromise = routeRequest(clusterStub, requestStub, responseStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls handler with the request", () => {
|
it("calls handler with the request", () => {
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -17,6 +17,7 @@ const staticFileRouteInjectable = getRouteInjectable({
|
|||||||
return route({
|
return route({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: `/{path*}`,
|
path: `/{path*}`,
|
||||||
|
requireAuthentication: false,
|
||||||
})(
|
})(
|
||||||
isDevelopment
|
isDevelopment
|
||||||
? di.inject(devStaticFileRouteHandlerInjectable)
|
? di.inject(devStaticFileRouteHandlerInjectable)
|
||||||
|
|||||||
33
src/renderer/auth/auth-header-state.injectable.ts
Normal file
33
src/renderer/auth/auth-header-state.injectable.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
const authHeaderValueStateInjectable = getInjectable({
|
||||||
|
id: "auth-header-value-state",
|
||||||
|
instantiate: () => {
|
||||||
|
let state: string | undefined = undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: () =>{
|
||||||
|
if (!state) {
|
||||||
|
throw new Error("Tried to get auth header value before it was initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
|
||||||
|
set: (newState: string) => {
|
||||||
|
if (state) {
|
||||||
|
throw new Error("Tried to overwrite existing state of auth header value");
|
||||||
|
}
|
||||||
|
|
||||||
|
state = newState;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authHeaderValueStateInjectable;
|
||||||
|
|
||||||
13
src/renderer/auth/auth-header.injectable.ts
Normal file
13
src/renderer/auth/auth-header.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 authHeaderValueStateInjectable from "./auth-header-state.injectable";
|
||||||
|
|
||||||
|
const authHeaderValueInjectable = getInjectable({
|
||||||
|
id: "auth-header-value",
|
||||||
|
instantiate: (di) => di.inject(authHeaderValueStateInjectable).get(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default authHeaderValueInjectable;
|
||||||
27
src/renderer/auth/init.injectable.ts
Normal file
27
src/renderer/auth/init.injectable.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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 { lensAuthenticationChannel } from "../../common/auth/channel";
|
||||||
|
import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token";
|
||||||
|
import requestFromChannelInjectable from "../utils/channel/request-from-channel.injectable";
|
||||||
|
import authHeaderValueStateInjectable from "./auth-header-state.injectable";
|
||||||
|
|
||||||
|
const initAuthHeaderValueStateInjectable = getInjectable({
|
||||||
|
id: "init-auth-header-value-state",
|
||||||
|
instantiate: (di) => {
|
||||||
|
const state = di.inject(authHeaderValueStateInjectable);
|
||||||
|
const requestFromChannel = di.inject(requestFromChannelInjectable);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "init-auth-header-value-state",
|
||||||
|
run: async () => {
|
||||||
|
state.set(await requestFromChannel(lensAuthenticationChannel));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
injectionToken: beforeFrameStartsInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default initAuthHeaderValueStateInjectable;
|
||||||
Loading…
Reference in New Issue
Block a user