diff --git a/app/entry.server.tsx b/app/entry.server.tsx index a1863be..2346e4b 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -3,17 +3,11 @@ import { createReadableStreamFromReadable } from '@react-router/node'; import { isbot } from 'isbot'; import type { RenderToPipeableStreamOptions } from 'react-dom/server'; import { renderToPipeableStream } from 'react-dom/server'; -import type { AppLoadContext, EntryContext } from 'react-router'; -import { ServerRouter } from 'react-router'; -import { hs_loadConfig } from '~/utils/config/loader'; -import { hp_storeContext } from '~/utils/headscale'; +import { EntryContext, ServerRouter } from 'react-router'; import { hp_loadLogger } from '~/utils/log'; -import { initSessionManager } from '~/utils/sessions.server'; import type { AppContext } from '~server/context/app'; export const streamTimeout = 5_000; - -// TODO: checkOidc export default function handleRequest( request: Request, responseStatusCode: number, @@ -23,15 +17,8 @@ export default function handleRequest( ) { const { context } = loadContext; return new Promise((resolve, reject) => { - initSessionManager( - context.server.cookie_secret, - context.server.cookie_secure, - ); - // This is a promise but we don't need to wait for it to finish // before we start rendering the shell since it only loads once. - hs_loadConfig(context); - hp_storeContext(context); hp_loadLogger(context.debug); let shellRendered = false; diff --git a/app/utils/config/loader.ts b/app/utils/config/loader.ts index 70c84e0..e9ed692 100644 --- a/app/utils/config/loader.ts +++ b/app/utils/config/loader.ts @@ -9,8 +9,6 @@ let runtimeYaml: Document | undefined = undefined; let runtimeConfig: HeadscaleConfig | undefined = undefined; let runtimePath: string | undefined = undefined; let runtimeMode: 'rw' | 'ro' | 'no' = 'no'; -let runtimeStrict = true; - const runtimeLock = mutex(); export type ConfigModes = @@ -42,9 +40,12 @@ export function hs_getConfig(): ConfigModes { }; } -export async function hs_loadConfig(context: HeadplaneConfig) { +export async function hs_loadConfig(path?: string, strict?: boolean) { + if (runtimeConfig !== undefined) { + return; + } + runtimeLock.acquire(); - const path = context.headscale.config_path; if (!path) { runtimeLock.release(); return; @@ -62,8 +63,7 @@ export async function hs_loadConfig(context: HeadplaneConfig) { return; } - runtimeStrict = context.headscale.config_strict ?? true; - const config = validateConfig(rawConfig, runtimeStrict); + const config = validateConfig(rawConfig, strict ?? true); if (!config) { runtimeMode = 'no'; } @@ -194,3 +194,6 @@ export async function hs_patchConfig(patches: PatchConfig[]) { await writeFile(runtimePath, runtimeYaml.toString(), 'utf8'); runtimeLock.release(); } + +// IMPORTANT THIS IS A SIDE EFFECT ON INIT +hs_loadConfig(__hs_context.config_path, __hs_context.config_strict); diff --git a/app/utils/headscale.ts b/app/utils/headscale.ts index f7cfb9e..34065a4 100644 --- a/app/utils/headscale.ts +++ b/app/utils/headscale.ts @@ -1,7 +1,5 @@ import log, { noContext } from '~/utils/log'; -import { AppContext } from '~server/context/app'; -type Context = AppContext['context']; export class HeadscaleError extends Error { status: number; @@ -21,21 +19,13 @@ export class FatalError extends Error { } } -let context: Context | undefined = undefined; -export function hp_storeContext(ctx: Context) { - if (context) { - return; - } - - context = ctx; +interface HeadscaleContext { + url: string; } +declare const global: typeof globalThis & { __hs_context: HeadscaleContext }; export async function healthcheck() { - if (!context) { - throw noContext(); - } - - const prefix = context.headscale.url; + const prefix = __hs_context.url; log.debug('APIC', 'GET /health'); const health = new URL('health', prefix); @@ -50,15 +40,11 @@ export async function healthcheck() { } export async function pull(url: string, key: string) { - if (!context) { - throw noContext(); - } - if (!key || key === 'undefined' || key.length === 0) { throw new Error('Missing API key, could this be a cookie setting issue?'); } - const prefix = context.headscale.url; + const prefix = __hs_context.url; log.debug('APIC', 'GET %s', `${prefix}/api/${url}`); const response = await fetch(`${prefix}/api/${url}`, { @@ -81,15 +67,11 @@ export async function pull(url: string, key: string) { } export async function post(url: string, key: string, body?: unknown) { - if (!context) { - throw noContext(); - } - if (!key || key === 'undefined' || key.length === 0) { throw new Error('Missing API key, could this be a cookie setting issue?'); } - const prefix = context.headscale.url; + const prefix = __hs_context.url; log.debug('APIC', 'POST %s', `${prefix}/api/${url}`); const response = await fetch(`${prefix}/api/${url}`, { @@ -114,15 +96,11 @@ export async function post(url: string, key: string, body?: unknown) { } export async function put(url: string, key: string, body?: unknown) { - if (!context) { - throw noContext(); - } - if (!key || key === 'undefined' || key.length === 0) { throw new Error('Missing API key, could this be a cookie setting issue?'); } - const prefix = context.headscale.url; + const prefix = __hs_context.url; log.debug('APIC', 'PUT %s', `${prefix}/api/${url}`); const response = await fetch(`${prefix}/api/${url}`, { @@ -147,15 +125,11 @@ export async function put(url: string, key: string, body?: unknown) { } export async function del(url: string, key: string) { - if (!context) { - throw noContext(); - } - if (!key || key === 'undefined' || key.length === 0) { throw new Error('Missing API key, could this be a cookie setting issue?'); } - const prefix = context.headscale.url; + const prefix = __hs_context.url; log.debug('APIC', 'DELETE %s', `${prefix}/api/${url}`); const response = await fetch(`${prefix}/api/${url}`, { diff --git a/app/utils/sessions.server.ts b/app/utils/sessions.server.ts index f7680c3..cf6ccf7 100644 --- a/app/utils/sessions.server.ts +++ b/app/utils/sessions.server.ts @@ -1,8 +1,4 @@ -import { - Session, - SessionStorage, - createCookieSessionStorage, -} from 'react-router'; +import { Session, createCookieSessionStorage } from 'react-router'; export type SessionData = { hsApiKey: string; @@ -23,42 +19,28 @@ type SessionFlashData = { error: string; }; -type SessionStore = SessionStorage; - -// TODO: Add args to this function to allow custom domain/config -let sessionStorage: SessionStore | null = null; -export function initSessionManager(secret: string, secure: boolean) { - if (sessionStorage) { - return; - } - - sessionStorage = createCookieSessionStorage({ - cookie: { - name: 'hp_sess', - httpOnly: true, - maxAge: 60 * 60 * 24, // 24 hours - path: '/', - sameSite: 'lax', - secrets: [secret], - secure, - }, - }); -} +// TODO: Domain config in cookies +const sessionStorage = createCookieSessionStorage< + SessionData, + SessionFlashData +>({ + cookie: { + name: 'hp_sess', + httpOnly: true, + maxAge: 60 * 60 * 24, // 24 hours + path: '/', + sameSite: 'lax', + secrets: [__cookie_context.cookie_secret], + secure: __cookie_context.cookie_secure, + }, +}); export function getSession(cookie: string | null) { - if (!sessionStorage) { - throw new Error('Session manager not initialized'); - } - return sessionStorage.getSession(cookie); } export type ServerSession = Session; export async function auth(request: Request) { - if (!sessionStorage) { - return false; - } - const cookie = request.headers.get('Cookie'); const session = await sessionStorage.getSession(cookie); if (!session.has('hsApiKey')) { @@ -69,17 +51,9 @@ export async function auth(request: Request) { } export function destroySession(session: Session) { - if (!sessionStorage) { - throw new Error('Session manager not initialized'); - } - return sessionStorage.destroySession(session); } export function commitSession(session: Session, opts?: { maxAge?: number }) { - if (!sessionStorage) { - throw new Error('Session manager not initialized'); - } - return sessionStorage.commitSession(session, opts); } diff --git a/server/context/loader.ts b/server/context/loader.ts index f1e0426..4b88099 100644 --- a/server/context/loader.ts +++ b/server/context/loader.ts @@ -6,6 +6,19 @@ import log, { hpServer_loadLogger } from '~server/utils/log'; import mutex from '~server/utils/mutex'; import { HeadplaneConfig, coalesceConfig, validateConfig } from './parser'; +declare global { + let __cookie_context: { + cookie_secret: string; + cookie_secure: boolean; + }; + + let __hs_context: { + url: string; + config_path?: string; + config_strict?: boolean; + }; +} + const envBool = type('string | undefined').pipe((v) => { return ['1', 'true', 'yes', 'on'].includes(v?.toLowerCase() ?? ''); }); @@ -33,7 +46,6 @@ export function hp_getConfig() { } const config = runtimeConfig; - runtimeLock.release(); return config; } @@ -106,6 +118,19 @@ export async function hp_loadConfig() { testOidc(config.oidc); } + // @ts-expect-error: If we remove globalThis we get a runtime error + globalThis.__cookie_context = { + cookie_secret: config.server.cookie_secret, + cookie_secure: config.server.cookie_secure, + }; + + // @ts-expect-error: If we remove globalThis we get a runtime error + globalThis.__hs_context = { + url: config.headscale.url, + config_path: config.headscale.config_path, + config_strict: config.headscale.config_strict, + }; + runtimeConfig = config; runtimeLock.release(); }