From b78990293859481ea00cd93e7c1c03fd15aa943b Mon Sep 17 00:00:00 2001 From: Tristan Bastian Date: Thu, 4 Apr 2024 00:35:15 +0200 Subject: [PATCH] feat(microservice): add support for dealing with cookies as a preparation for allowing to login into a c8y edge instance Signed-off-by: Tristan Bastian --- backend/src/header-adjustment.ts | 14 ++++++-- backend/src/index.ts | 60 +++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/backend/src/header-adjustment.ts b/backend/src/header-adjustment.ts index 9ca5dbd..5e03f5e 100644 --- a/backend/src/header-adjustment.ts +++ b/backend/src/header-adjustment.ts @@ -1,6 +1,7 @@ import { ConnectionDetails } from "./connection-details"; import { IncomingHttpHeaders } from "http"; import { name, version } from "../package.json"; +import * as cookieLib from "cookie-parse"; export class HeaderAdjustment { private static headersToRemove = []; @@ -30,6 +31,12 @@ export class HeaderAdjustment { const newKey = key.replace(this.headerPrefix, "").replace(suffix, ""); keysToAdd[newKey] = value; } + if (headers.cookie) { + const {'XSRF-TOKEN': xsrf} = cookieLib.parse(headers.cookie || ""); + if (xsrf) { + keysToAdd['x-xsrf-token'] = xsrf; + } + } Object.assign(headers, keysToAdd); } @@ -56,13 +63,13 @@ export class HeaderAdjustment { // we want to keep the authorization header if the actual auth was cookie based // but we want to remove the fake basic auth header added by the microservice proxy - if (!headers.authorization?.startsWith('Basic ')) { + if (!headers.authorization?.startsWith("Basic ")) { return; } const token = headers.authorization.replace(/^Basic\s/, ""); const decodedToken = Buffer.from(token, "base64").toString("utf-8"); - if (decodedToken.endsWith(':')) { + if (decodedToken.endsWith(":")) { delete headers.authorization; } } @@ -73,9 +80,10 @@ export class HeaderAdjustment { } const cookieKeysToReplace = ["authorization", "XSRF-TOKEN"]; - return cookieKeysToReplace.reduceRight((prev, curr) => { + const replacedCookies = cookieKeysToReplace.reduceRight((prev, curr) => { return this.removeCookieByName(prev, curr); }, currentCookieValue); + return replacedCookies.replaceAll("cloud-http-proxy-", ""); } private static removeCookieByName(cookieValue: string, name: string) { diff --git a/backend/src/index.ts b/backend/src/index.ts index 2f1cf95..b27051f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -10,6 +10,7 @@ import { HeaderAdjustment } from "./header-adjustment"; import { RCAServerStore } from "./rca-server-store"; import Agent from "agentkeepalive"; import { HttpsAgent } from "agentkeepalive"; +import * as http from "http"; dotenv.config(); @@ -45,6 +46,14 @@ app.get("/health", (req, res) => { }); }); +app.use((req, res, next) => { + if (req.headers.authorization || req.headers.cookie?.includes('authorization')) { + return next(); + } + res.setHeader('WWW-Authenticate', 'Basic realm="My Realm"') + res.status(401).send(); +}); + async function getTarget( req: express.Request< { @@ -63,6 +72,7 @@ async function getTarget( } catch (e) { requestLogger.error("Failed to retrieve details", { e, + errorMessage: e.errorMessage, headers: req.headers, }); throw e; @@ -96,23 +106,40 @@ function hostRewrite(req: express.Request, secure: boolean) { function getRewriteOptions( req: express.Request, - secure?: boolean + secure?: boolean, + hasCustomHost?: boolean ): Server.ServerOptions { + const { "x-forwarded-host": forwardedHost } = req.headers; return { autoRewrite: false, hostRewrite: hostRewrite(req, secure), - changeOrigin: true, + changeOrigin: !hasCustomHost, protocolRewrite: (req.headers["x-forwarded-proto"] as string) || "http", + cookieDomainRewrite: + typeof forwardedHost === "string" ? `.${forwardedHost.replace(/:.*$/, '')}` : undefined, + cookiePathRewrite: `/service/cloud-http-proxy${secure ? '/s' : ''}/${req.params.cloudProxyDeviceId}/${req.params.cloudProxyConfigId}/`, }; } -app.use((req, res, next) => { - if (req.headers.authorization || req.headers.cookie?.includes('authorization')) { - return next(); - } - res.setHeader('WWW-Authenticate', 'Basic realm="My Realm"') - res.status(401).send(); -}) +function hasCustomHostHeader(req: express.Request, deviceId: string, configId: string) { + const headerToLookoutFor = `rca-http-header-host-${deviceId}-${configId}`; + return !!req.headers[headerToLookoutFor]; +} + +function prefixCookiesToBeSet(proxy: Server>) { + proxy.on('proxyRes', (response) => { + const cookiesToSet = response.headers["set-cookie"]; + if (!cookiesToSet?.length) { + return; + } + + const adjustedCookies = cookiesToSet.map((cookie: string) => { + return `cloud-http-proxy-${cookie}`; + }); + + response.headers["set-cookie"] = adjustedCookies; + }); +} app.use("/s/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => { const requestLogger = logger.child({ @@ -120,14 +147,16 @@ app.use("/s/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => { url: req.url, params: req.params, }); + try { + const hasCustomHost = hasCustomHostHeader(req, req.params.cloudProxyDeviceId, req.params.cloudProxyConfigId); const target = await getTarget(req, requestLogger, true); - const rewrietOptions = getRewriteOptions(req, true); + const rewriteOptions = getRewriteOptions(req, true, hasCustomHost); const proxy = createProxyServer({ target, agent: agents.https, secure: false, - ...rewrietOptions, + ...rewriteOptions, }); if (req.headers.upgrade) { @@ -138,6 +167,8 @@ app.use("/s/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => { return; } + prefixCookiesToBeSet(proxy); + proxy.web( req, res, @@ -166,12 +197,13 @@ app.use("/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => { }); try { + const hasCustomHost = hasCustomHostHeader(req, req.params.cloudProxyDeviceId, req.params.cloudProxyConfigId); const target = await getTarget(req, requestLogger); - const rewrietOptions = getRewriteOptions(req, true); + const rewriteOptions = getRewriteOptions(req, true, hasCustomHost); const proxy = createProxyServer({ target, agent: agents.http, - ...rewrietOptions + ...rewriteOptions, }); if (req.headers.upgrade) { @@ -182,6 +214,8 @@ app.use("/:cloudProxyDeviceId/:cloudProxyConfigId/", async (req, res) => { return; } + prefixCookiesToBeSet(proxy); + proxy.web( req, res,