feat: reach an initial working stage

This commit is contained in:
Aarnav Tale
2025-03-22 01:36:27 -04:00
parent 8db323b63f
commit 34cfee7cff
34 changed files with 1078 additions and 684 deletions
+24 -19
View File
@@ -22,24 +22,29 @@ export type ConfigModes =
config: undefined;
};
export function hs_getConfig(): ConfigModes {
if (runtimeMode === 'no') {
return {
mode: 'no',
config: undefined,
};
}
// export function hs_getConfig(): ConfigModes {
// return {
// mode: 'no',
// config: undefined,
// };
runtimeLock.acquire();
// We can assert if mode is not 'no'
const config = runtimeConfig!;
runtimeLock.release();
// if (runtimeMode === 'no') {
// return {
// mode: 'no',
// config: undefined,
// };
// }
return {
mode: runtimeMode,
config: config,
};
}
// runtimeLock.acquire();
// // We can assert if mode is not 'no'
// const config = runtimeConfig!;
// runtimeLock.release();
// return {
// mode: runtimeMode,
// config: config,
// };
// }
export async function hs_loadConfig(path?: string, strict?: boolean) {
if (runtimeConfig !== undefined) {
@@ -199,6 +204,6 @@ export async function hs_patchConfig(patches: PatchConfig[]) {
// IMPORTANT THIS IS A SIDE EFFECT ON INIT
// TODO: Replace this into the new singleton system
const context = hp_getConfig();
hs_loadConfig(context.headscale.config_path, context.headscale.config_strict);
hp_getIntegration();
// const context = hp_getConfig();
// hs_loadConfig(context.headscale.config_path, context.headscale.config_strict);
// hp_getIntegration();
-149
View File
@@ -1,149 +0,0 @@
import { request } from 'undici';
import { hp_getConfig, hp_getSingleton } from '~server/context/global';
import log from '~server/utils/log';
export class HeadscaleError extends Error {
status: number;
constructor(message: string, status: number) {
super(message);
this.name = 'HeadscaleError';
this.status = status;
}
}
export class FatalError extends Error {
constructor() {
super(
'The Headscale server is not accessible or the supplied API key is invalid',
);
this.name = 'FatalError';
}
}
export async function healthcheck() {
log.debug('APIC', 'GET /health');
const health = new URL('health', hp_getConfig().headscale.url);
const response = await request(health.toString(), {
dispatcher: hp_getSingleton('api_agent'),
headers: {
Accept: 'application/json',
},
});
// Intentionally not catching
return response.statusCode === 200;
}
export async function pull<T>(url: string, key: string) {
if (!key || key === 'undefined' || key.length === 0) {
throw new Error('Missing API key, could this be a cookie setting issue?');
}
const prefix = hp_getConfig().headscale.url;
log.debug('APIC', 'GET %s', `${prefix}/api/${url}`);
const response = await request(`${prefix}/api/${url}`, {
dispatcher: hp_getSingleton('api_agent'),
headers: {
Authorization: `Bearer ${key}`,
},
});
if (response.statusCode >= 400) {
log.debug(
'APIC',
'GET %s failed with status %d',
`${prefix}/api/${url}`,
response.statusCode,
);
throw new HeadscaleError(await response.body.text(), response.statusCode);
}
return response.body.json() as Promise<T>;
}
export async function post<T>(url: string, key: string, body?: unknown) {
if (!key || key === 'undefined' || key.length === 0) {
throw new Error('Missing API key, could this be a cookie setting issue?');
}
const prefix = hp_getConfig().headscale.url;
log.debug('APIC', 'POST %s', `${prefix}/api/${url}`);
const response = await request(`${prefix}/api/${url}`, {
dispatcher: hp_getSingleton('api_agent'),
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
headers: {
Authorization: `Bearer ${key}`,
},
});
if (response.statusCode >= 400) {
log.debug(
'APIC',
'POST %s failed with status %d',
`${prefix}/api/${url}`,
response.statusCode,
);
throw new HeadscaleError(await response.body.text(), response.statusCode);
}
return response.body.json() as Promise<T>;
}
export async function put<T>(url: string, key: string, body?: unknown) {
if (!key || key === 'undefined' || key.length === 0) {
throw new Error('Missing API key, could this be a cookie setting issue?');
}
const prefix = hp_getConfig().headscale.url;
log.debug('APIC', 'PUT %s', `${prefix}/api/${url}`);
const response = await request(`${prefix}/api/${url}`, {
dispatcher: hp_getSingleton('api_agent'),
method: 'PUT',
body: body ? JSON.stringify(body) : undefined,
headers: {
Authorization: `Bearer ${key}`,
},
});
if (response.statusCode >= 400) {
log.debug(
'APIC',
'PUT %s failed with status %d',
`${prefix}/api/${url}`,
response.statusCode,
);
throw new HeadscaleError(await response.body.text(), response.statusCode);
}
return response.body.json() as Promise<T>;
}
export async function del<T>(url: string, key: string) {
if (!key || key === 'undefined' || key.length === 0) {
throw new Error('Missing API key, could this be a cookie setting issue?');
}
const prefix = hp_getConfig().headscale.url;
log.debug('APIC', 'DELETE %s', `${prefix}/api/${url}`);
const response = await request(`${prefix}/api/${url}`, {
dispatcher: hp_getSingleton('api_agent'),
method: 'DELETE',
headers: {
Authorization: `Bearer ${key}`,
},
});
if (response.statusCode >= 400) {
log.debug(
'APIC',
'DELETE %s failed with status %d',
`${prefix}/api/${url}`,
response.statusCode,
);
throw new HeadscaleError(await response.body.text(), response.statusCode);
}
return response.body.json() as Promise<T>;
}
+33 -35
View File
@@ -2,11 +2,11 @@ import { hp_getConfig } from '~server/context/global';
import { HeadplaneConfig } from '~server/context/parser';
import log from '~server/utils/log';
import { Integration } from './abstract';
import dockerIntegration from './docker';
import kubernetesIntegration from './kubernetes';
import procIntegration from './proc';
// import dockerIntegration from './docker';
// import kubernetesIntegration from './kubernetes';
// import procIntegration from './proc';
let runtimeIntegration: Integration<unknown> | undefined = undefined;
const runtimeIntegration: Integration<unknown> | undefined = undefined;
export function hp_getIntegration() {
return runtimeIntegration;
@@ -15,24 +15,22 @@ export function hp_getIntegration() {
export async function hp_loadIntegration(
context: HeadplaneConfig['integration'],
) {
const integration = getIntegration(context);
if (!integration) {
return;
}
try {
const res = await integration.isAvailable();
if (!res) {
log.error('INTG', 'Integration %s is not available', integration);
return;
}
} catch (error) {
log.error('INTG', 'Failed to load integration %s: %s', integration, error);
log.debug('INTG', 'Loading error: %o', error);
return;
}
runtimeIntegration = integration;
// const integration = getIntegration(context);
// if (!integration) {
// return;
// }
// try {
// const res = await integration.isAvailable();
// if (!res) {
// log.error('INTG', 'Integration %s is not available', integration);
// return;
// }
// } catch (error) {
// log.error('INTG', 'Failed to load integration %s: %s', integration, error);
// log.debug('INTG', 'Loading error: %o', error);
// return;
// }
// runtimeIntegration = integration;
}
function getIntegration(integration: HeadplaneConfig['integration']) {
@@ -50,23 +48,23 @@ function getIntegration(integration: HeadplaneConfig['integration']) {
return;
}
if (docker?.enabled) {
log.info('INTG', 'Using Docker integration');
return new dockerIntegration(integration?.docker);
}
// if (docker?.enabled) {
// log.info('INTG', 'Using Docker integration');
// return new dockerIntegration(integration?.docker);
// }
if (k8s?.enabled) {
log.info('INTG', 'Using Kubernetes integration');
return new kubernetesIntegration(integration?.kubernetes);
}
// if (k8s?.enabled) {
// log.info('INTG', 'Using Kubernetes integration');
// return new kubernetesIntegration(integration?.kubernetes);
// }
if (proc?.enabled) {
log.info('INTG', 'Using Proc integration');
return new procIntegration(integration?.proc);
}
// if (proc?.enabled) {
// log.info('INTG', 'Using Proc integration');
// return new procIntegration(integration?.proc);
// }
}
// IMPORTANT THIS IS A SIDE EFFECT ON INIT
// TODO: Switch this to the new singleton system
const context = hp_getConfig();
hp_loadIntegration(context.integration);
// hp_loadIntegration(context.integration);
+13 -10
View File
@@ -1,13 +1,11 @@
import { readFile } from 'node:fs/promises';
import * as client from 'openid-client';
import { Configuration } from 'openid-client';
import { hp_getSingleton, hp_setSingleton } from '~server/context/global';
import { HeadplaneConfig } from '~server/context/parser';
import log from '~server/utils/log';
type OidcConfig = NonNullable<HeadplaneConfig['oidc']>;
declare global {
const __PREFIX__: string;
}
// We try our best to infer the callback URI of our Headplane instance
// By default it is always /<base_path>/oidc/callback
@@ -103,8 +101,11 @@ function clientAuthMethod(
}
}
export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
const config = hp_getSingleton('oidc_client');
export async function beginAuthFlow(
config: Configuration,
redirect_uri: string,
token_endpoint_auth_method: string,
) {
const codeVerifier = client.randomPKCECodeVerifier();
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
@@ -113,7 +114,7 @@ export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
token_endpoint_auth_method: oidc.token_endpoint_auth_method,
token_endpoint_auth_method,
state: client.randomState(),
};
@@ -134,18 +135,20 @@ export async function beginAuthFlow(oidc: OidcConfig, redirect_uri: string) {
interface FlowOptions {
redirect_uri: string;
codeVerifier: string;
code_verifier: string;
state: string;
nonce?: string;
}
export async function finishAuthFlow(oidc: OidcConfig, options: FlowOptions) {
const config = hp_getSingleton('oidc_client');
export async function finishAuthFlow(
config: Configuration,
options: FlowOptions,
) {
const tokens = await client.authorizationCodeGrant(
config,
new URL(options.redirect_uri),
{
pkceCodeVerifier: options.codeVerifier,
pkceCodeVerifier: options.code_verifier,
expectedNonce: options.nonce,
expectedState: options.state,
idTokenExpected: true,
-63
View File
@@ -1,63 +0,0 @@
import { Session, createCookieSessionStorage } from 'react-router';
import { hp_getConfig } from '~server/context/global';
export type SessionData = {
hsApiKey: string;
oidc_state: string;
oidc_code_verif: string;
oidc_nonce: string;
oidc_redirect_uri: string;
agent_onboarding: boolean;
user: {
subject: string;
name: string;
email?: string;
username?: string;
picture?: string;
};
};
type SessionFlashData = {
error: string;
};
// TODO: Domain config in cookies
// TODO: Move this to the singleton system
const context = hp_getConfig();
const sessionStorage = createCookieSessionStorage<
SessionData,
SessionFlashData
>({
cookie: {
name: 'hp_sess',
httpOnly: true,
maxAge: 60 * 60 * 24, // 24 hours
path: '/',
sameSite: 'lax',
secrets: [context.server.cookie_secret],
secure: context.server.cookie_secure,
},
});
export function getSession(cookie: string | null) {
return sessionStorage.getSession(cookie);
}
export type ServerSession = Session<SessionData, SessionFlashData>;
export async function auth(request: Request) {
const cookie = request.headers.get('Cookie');
const session = await sessionStorage.getSession(cookie);
if (!session.has('hsApiKey')) {
return false;
}
return session;
}
export function destroySession(session: Session) {
return sessionStorage.destroySession(session);
}
export function commitSession(session: Session, opts?: { maxAge?: number }) {
return sessionStorage.commitSession(session, opts);
}