diff --git a/tunnel-server/index.ts b/tunnel-server/index.ts index 1064f136..3946a56e 100644 --- a/tunnel-server/index.ts +++ b/tunnel-server/index.ts @@ -13,7 +13,7 @@ import { appLoggerFromEnv } from './src/logging' import { tunnelsGauge, runMetricsServer } from './src/metrics' import { numberFromEnv, requiredEnv } from './src/env' import { replaceHostname } from './src/url' -import { sessionManager } from './src/seesion' +import { session } from './src/seesion' import { claimsSchema } from './src/auth' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) @@ -37,10 +37,10 @@ const BASE_URL = (() => { })() const envStore = inMemoryPreviewEnvStore() -const appSessionManager = sessionManager({ domain: BASE_URL.hostname, schema: claimsSchema, keys: process.env.COOKIE_SECRETS?.split(' ') }) +const appSessionManager = session({ domain: BASE_URL.hostname, schema: claimsSchema, keys: process.env.COOKIE_SECRETS?.split(' ') }) const loginUrl = new URL('/login', replaceHostname(BASE_URL, `auth.${BASE_URL.hostname}`)).toString() const app = createApp({ - sessionManager: appSessionManager, + session: appSessionManager, envStore, baseUrl: BASE_URL, isProxyRequest: isProxyRequest(BASE_URL.hostname), diff --git a/tunnel-server/src/app.ts b/tunnel-server/src/app.ts index ed67ab1b..04310076 100644 --- a/tunnel-server/src/app.ts +++ b/tunnel-server/src/app.ts @@ -4,16 +4,16 @@ import http from 'http' import internal from 'stream' import { Logger } from 'pino' import { match } from 'ts-pattern' -import { SessionManager } from './seesion' +import { Session } from './seesion' import { Claims, JwtAuthenticator, authenticator, getIssuerToKeyDataFromEnv, unauthorized } from './auth' import { PreviewEnvStore } from './preview-env' import { replaceHostname } from './url' -export const app = ({ isProxyRequest, proxyHandlers, sessionManager, baseUrl, envStore, logger }: { +export const app = ({ isProxyRequest, proxyHandlers, session: sessionManager, baseUrl, envStore, logger }: { isProxyRequest: (req: http.IncomingMessage) => boolean logger: Logger baseUrl: URL - sessionManager: SessionManager + session: Session envStore: PreviewEnvStore proxyHandlers: { wsHandler: (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => void diff --git a/tunnel-server/src/preview-env.ts b/tunnel-server/src/preview-env.ts index 854884d0..d638b710 100644 --- a/tunnel-server/src/preview-env.ts +++ b/tunnel-server/src/preview-env.ts @@ -11,21 +11,27 @@ export type PreviewEnv = { export type PreviewEnvStore = { get: (key: string) => Promise + getByPkThumbprint: (pkThumbprint: string) => Promise set: (key: string, env: PreviewEnv) => Promise has: (key: string) => Promise delete: (key: string) => Promise - getAll: () => Promise<{[key: string]: PreviewEnv}> } -export const inMemoryPreviewEnvStore = (initial?: Record): PreviewEnvStore => { - const map = new Map(Object.entries(initial ?? {})) +export const inMemoryPreviewEnvStore = (): PreviewEnvStore => { + const tunnelNameToEnv = new Map() + const pkThumbprintToEnv = new Map() + return { - get: async key => map.get(key), + get: async key => tunnelNameToEnv.get(key), + getByPkThumbprint: async pkThumbprint => pkThumbprintToEnv.get(pkThumbprint), set: async (key, value) => { - map.set(key, value) + tunnelNameToEnv.set(key, value) + pkThumbprintToEnv.set( + value.publicKeyThumbprint, + [...pkThumbprintToEnv.get(value.publicKeyThumbprint) ?? [], value] + ) }, - has: async key => map.has(key), - delete: async key => map.delete(key), - getAll: async () => Object.fromEntries< PreviewEnv>(map), + has: async key => tunnelNameToEnv.has(key), + delete: async key => tunnelNameToEnv.delete(key), } } diff --git a/tunnel-server/src/proxy.ts b/tunnel-server/src/proxy.ts index 59d59e81..042110e8 100644 --- a/tunnel-server/src/proxy.ts +++ b/tunnel-server/src/proxy.ts @@ -4,8 +4,8 @@ import internal from 'stream' import type { Logger } from 'pino' import { PreviewEnvStore } from './preview-env' import { requestsCounter } from './metrics' -import { Claims, authenticator, JwtAuthenticator, unauthorized, envToIssuerToKeyData } from './auth' -import { SessionManager } from './seesion' +import { Claims, authenticator, JwtAuthenticator, unauthorized, getIssuerToKeyDataFromEnv } from './auth' +import { Session } from './seesion' export const isProxyRequest = ( hostname: string, @@ -44,7 +44,7 @@ export function proxyHandlers({ sessionManager, logger, }: { - sessionManager: SessionManager + sessionManager: Session envStore: PreviewEnvStore loginUrl: string logger: Logger @@ -74,7 +74,7 @@ export function proxyHandlers({ const session = sessionManager(req, res, env.publicKeyThumbprint) if (env.access === 'private') { if (!session.user) { - const authenticate = authenticator([JwtAuthenticator(envToIssuerToKeyData(env))]) + const authenticate = authenticator([JwtAuthenticator(getIssuerToKeyDataFromEnv(env))]) try { const authResult = await authenticate(req) if (!authResult.isAuthenticated) { diff --git a/tunnel-server/src/seesion.ts b/tunnel-server/src/seesion.ts index 662d1987..8668b38d 100644 --- a/tunnel-server/src/seesion.ts +++ b/tunnel-server/src/seesion.ts @@ -1,35 +1,35 @@ import { IncomingMessage, ServerResponse } from 'http' import Cookies from 'cookies' import { randomBytes } from 'crypto' -import * as z from "zod" +import * as z from 'zod' // for testing, for production workload use the env var COOKIE_SECRETS -function generateSecret(){ +function generateSecret() { return randomBytes(32) .toString('base64') .slice(0, 32) } -export function sessionManager(opts: {domain: string, schema: z.ZodSchema, keys?: string[] }){ - const keys = opts.keys ?? [generateSecret()] - return function session(req: IncomingMessage, res: ServerResponse, thumbprint: string){ - const cookies = new Cookies(req, res, { - secure: true, - keys - }) - const data = cookies.get(`preevy-${thumbprint}`, {signed: true}); - let currentUser = data ? opts.schema.parse(JSON.parse(data)) : undefined - const session = { - get user(){ return currentUser}, - set(user: T){ - currentUser = user - }, - save: ()=> { - cookies.set(`preevy-${thumbprint}`, JSON.stringify(currentUser), {domain: opts.domain, signed: true}) - } - } - return session +export function session(opts: {domain: string; schema: z.ZodSchema; keys?: string[] }) { + const keys = opts.keys ?? [generateSecret()] + return function getSession(req: IncomingMessage, res: ServerResponse, thumbprint: string) { + const cookies = new Cookies(req, res, { + secure: true, + keys, + }) + const data = cookies.get(`preevy-${thumbprint}`, { signed: true }) + let currentUser = data ? opts.schema.parse(JSON.parse(data)) : undefined + + return { + get user() { return currentUser }, + set(user: T) { + currentUser = user + }, + save: () => { + cookies.set(`preevy-${thumbprint}`, JSON.stringify(currentUser), { domain: opts.domain, signed: true }) + }, } + } } -export type SessionManager = ReturnType> \ No newline at end of file +export type Session = ReturnType>