headplane/app/utils/config.ts

354 lines
6.7 KiB
TypeScript
Raw Normal View History

2024-05-15 21:54:40 -04:00
import { type Document, parse, parseDocument } from 'yaml'
2024-03-30 04:20:16 -04:00
import { type FSWatcher, watch } from 'node:fs'
import { access, constants, readFile, writeFile } from 'node:fs/promises'
2024-03-26 16:55:20 -04:00
import { resolve } from 'node:path'
type Duration = `${string}s` | `${string}h` | `${string}m` | `${string}d` | `${string}y`
2024-05-15 21:54:40 -04:00
interface Config {
server_url: string
listen_addr: string
metrics_listen_addr: string
grpc_listen_addr: string
grpc_allow_insecure: boolean
2024-03-26 16:55:20 -04:00
2024-05-15 21:54:40 -04:00
private_key_path: string
2024-03-26 16:55:20 -04:00
noise: {
2024-05-15 21:54:40 -04:00
private_key_path: string
}
2024-03-26 16:55:20 -04:00
prefixes: {
2024-05-15 21:54:40 -04:00
v4: string
v6: string
}
2024-03-26 16:55:20 -04:00
derp: {
server: {
2024-05-15 21:54:40 -04:00
enabled: boolean
region_id: number
region_code: string
region_name: string
stun_listen_addr: string
}
urls: string[]
paths: string[]
auto_update_enabled: boolean
update_frequency: Duration
}
disable_check_updates: boolean
epheremal_node_inactivity_timeout: Duration
node_update_check_interval: Duration
2024-03-26 16:55:20 -04:00
// Database is probably dangerous
database: {
2024-05-15 21:54:40 -04:00
type: 'sqlite3' | 'sqlite' | 'postgres'
2024-03-26 16:55:20 -04:00
sqlite?: {
2024-05-15 21:54:40 -04:00
path: string
}
2024-03-26 16:55:20 -04:00
postgres?: {
2024-05-15 21:54:40 -04:00
host: string
port: number
name: string
user: string
pass: string
max_open_conns: number
max_idle_conns: number
conn_max_idle_time_secs: number
ssl: boolean
}
}
acme_url: string
acme_email: string
tls_letsencrypt_hostname: string
tls_letsencrypt_cache_dir: string
tls_letsencrypt_challenge_type: string
tls_letsencrypt_listen: string
tls_cert_path: string
tls_key_path: string
2024-03-26 16:55:20 -04:00
log: {
2024-05-15 21:54:40 -04:00
format: 'text' | 'json'
level: string
}
2024-03-26 16:55:20 -04:00
2024-05-15 21:54:40 -04:00
acl_policy_path: string
2024-03-26 16:55:20 -04:00
dns_config: {
2024-05-15 21:54:40 -04:00
override_local_dns: boolean
nameservers: string[]
restricted_nameservers: Record<string, string[]> // Split DNS
domains: string[]
extra_records: {
name: string
type: 'A'
value: string
}[]
magic_dns: boolean
base_domain: string
}
unix_socket: string
unix_socket_permission: string
2024-03-26 16:55:20 -04:00
oidc: {
2024-05-15 21:54:40 -04:00
only_start_if_oidc_is_available: boolean
issuer: string
client_id: string
client_secret: string
expiry: Duration
use_expiry_from_token: boolean
scope: string[]
extra_params: Record<string, string>
allowed_domains: string[]
allowed_groups: string[]
allowed_users: string[]
strip_email_domain: boolean
}
2024-03-26 16:55:20 -04:00
logtail: {
2024-05-15 21:54:40 -04:00
enabled: boolean
}
2024-03-26 16:55:20 -04:00
2024-05-15 21:54:40 -04:00
randomize_client_port: boolean
2024-03-26 16:55:20 -04:00
}
2024-03-28 17:03:37 -04:00
let config: Document
2024-03-26 16:55:20 -04:00
export async function getConfig(force = false) {
if (!config || force) {
2024-03-26 16:55:20 -04:00
const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
const data = await readFile(path, 'utf8')
2024-03-28 17:03:37 -04:00
config = parseDocument(data)
2024-03-26 16:55:20 -04:00
}
2024-03-28 17:03:37 -04:00
return config.toJSON() as Config
2024-03-26 16:55:20 -04:00
}
2024-03-28 17:03:37 -04:00
export async function getAcl() {
let path = process.env.ACL_FILE
if (!path) {
try {
const config = await getConfig()
path = config.acl_policy_path
} catch {}
}
if (!path) {
2024-04-17 17:20:35 -04:00
return { data: '', type: 'json' }
}
const data = await readFile(path, 'utf8')
2024-04-17 17:20:35 -04:00
// Naive check for YAML over JSON
// This is because JSON.parse doesn't support comments
try {
parse(data)
return { data, type: 'yaml' }
} catch {
return { data, type: 'json' }
}
}
2024-03-28 17:03:37 -04:00
// This is so obscenely dangerous, please have a check around it
export async function patchConfig(partial: Record<string, unknown>) {
for (const [key, value] of Object.entries(partial)) {
config.setIn(key.split('.'), value)
}
const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
await writeFile(path, config.toString(), 'utf8')
}
export async function patchAcl(data: string) {
let path = process.env.ACL_FILE
if (!path) {
try {
const config = await getConfig()
path = config.acl_policy_path
} catch {}
}
if (!path) {
throw new Error('No ACL file defined')
}
await writeFile(path, data, 'utf8')
}
let watcher: FSWatcher
export function registerConfigWatcher() {
if (watcher) {
return
}
const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
2024-03-30 04:20:16 -04:00
watcher = watch(path, async () => {
console.log('Config file changed, reloading')
await getConfig(true)
})
}
2024-05-15 21:54:40 -04:00
export interface Context {
hasDockerSock: boolean
hasConfig: boolean
hasConfigWrite: boolean
hasAcl: boolean
hasAclWrite: boolean
headscaleUrl: string
oidcConfig?: {
2024-05-15 21:54:40 -04:00
issuer: string
client: string
secret: string
}
2024-03-28 17:03:37 -04:00
}
export let context: Context
export async function getContext() {
if (!context) {
context = {
hasDockerSock: await checkSock(),
2024-03-30 01:48:35 -04:00
hasConfig: await hasConfig(),
hasConfigWrite: await hasConfigW(),
hasAcl: await hasAcl(),
hasAclWrite: await hasAclW(),
headscaleUrl: await getHeadscaleUrl(),
2024-05-15 21:54:40 -04:00
oidcConfig: await getOidcConfig(),
2024-03-28 17:03:37 -04:00
}
}
return context
}
async function getOidcConfig() {
// Check for the OIDC environment variables first
let issuer = process.env.OIDC_ISSUER
let client = process.env.OIDC_CLIENT_ID
let secret = process.env.OIDC_CLIENT_SECRET
const rootKey = process.env.API_KEY
if (!issuer || !client || !secret) {
const config = await getConfig()
2024-05-15 21:54:40 -04:00
issuer = config.oidc.issuer
client = config.oidc.client_id
secret = config.oidc.client_secret
}
// If atleast one is defined but not all 3, throw an error
if ((issuer || client || secret) && !(issuer && client && secret)) {
throw new Error('OIDC configuration is incomplete')
}
if (!issuer || !client || !secret) {
return
}
if (!rootKey) {
throw new Error('Cannot use OIDC without the root API_KEY variable set')
}
return { issuer, client, secret }
}
async function getHeadscaleUrl() {
if (process.env.HEADSCALE_URL) {
return process.env.HEADSCALE_URL
}
try {
const config = await getConfig()
if (config.server_url) {
return config.server_url
}
} catch {}
return ''
}
2024-03-30 01:48:35 -04:00
async function checkSock() {
2024-03-28 17:03:37 -04:00
try {
2024-03-30 01:48:35 -04:00
await access('/var/run/docker.sock', constants.R_OK)
2024-03-28 17:03:37 -04:00
return true
2024-03-30 01:48:35 -04:00
} catch {}
if (!process.env.HEADSCALE_CONTAINER) {
return false
2024-03-28 17:03:37 -04:00
}
return false
}
2024-03-30 01:48:35 -04:00
async function hasConfig() {
2024-03-28 17:03:37 -04:00
try {
2024-03-30 01:48:35 -04:00
await getConfig()
2024-03-28 17:03:37 -04:00
return true
} catch {}
2024-03-30 01:48:35 -04:00
return false
}
async function hasConfigW() {
const path = resolve(process.env.CONFIG_FILE ?? '/etc/headscale/config.yaml')
try {
await access(path, constants.W_OK)
return true
} catch {}
return false
}
async function hasAcl() {
let path = process.env.ACL_FILE
if (!path) {
try {
const config = await getConfig()
path = config.acl_policy_path
} catch {}
}
if (!path) {
return false
}
try {
2024-03-30 01:56:06 -04:00
path = resolve(path)
2024-03-30 01:48:35 -04:00
await access(path, constants.R_OK)
return true
} catch (error) {
console.log('Cannot acquire read access to ACL file', error)
}
2024-03-30 01:48:35 -04:00
return false
}
async function hasAclW() {
let path = process.env.ACL_FILE
if (!path) {
try {
const config = await getConfig()
path = config.acl_policy_path
} catch {}
}
2024-03-28 17:03:37 -04:00
2024-03-30 01:48:35 -04:00
if (!path) {
return false
}
try {
2024-03-30 01:56:06 -04:00
path = resolve(path)
2024-03-30 01:48:35 -04:00
await access(path, constants.W_OK)
return true
} catch (error) {
console.log('Cannot acquire read access to ACL file', error)
}
2024-03-30 01:48:35 -04:00
2024-03-28 17:03:37 -04:00
return false
}