mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
The public relay used to accept a shared auth key compiled into every hub, so its bandwidth was open to anyone. The relay now issues a per-hub credential it can meter and revoke, and hubs obtain one on their own. - --relay resolves an auth key at startup: HAPI_RELAY_AUTH env, then a key persisted in settings.json, then a fresh key from the relay's /issue endpoint. There is no shared-key fallback; if no key can be obtained the tunnel does not start and the hub says why. - A persisted key rejected by the relay (HTTP 403 after revocation or a secret rotation) is discarded and replaced once, then the tunnel is restarted, so a revoked hub recovers without manual edits. Keys given explicitly through the environment are never overwritten. - Issuance is rate-limited per public IP; HTTP 429 is reported with the retry hint instead of being retried blindly, which matters for users sharing a CGNAT or corporate egress address. - The tunnel URL now comes from upstream tunwg's slog JSON on stderr (msg="listener started"), replacing the fork's custom --json event, and --log_level=0 keeps per-request logs out of the hub console. Requires a relay running tunwg with TUNWG_AUTH_SECRET configured.
207 lines
7.4 KiB
TypeScript
207 lines
7.4 KiB
TypeScript
/**
|
|
* Configuration for hapi-hub (Direct Connect)
|
|
*
|
|
* Configuration is loaded with priority: environment variable > settings.json > default
|
|
* When values are read from environment variables and not present in settings.json,
|
|
* they are automatically saved for future use
|
|
*
|
|
* Optional environment variables:
|
|
* - CLI_API_TOKEN: Shared secret for hapi CLI authentication (auto-generated if not set)
|
|
* - TELEGRAM_BOT_TOKEN: Telegram Bot API token from @BotFather
|
|
* - TELEGRAM_NOTIFICATION: Enable/disable Telegram notifications (default: true)
|
|
* - SERVERCHAN_SENDKEY: Server酱 SendKey/AppKey for push notifications
|
|
* - SERVERCHAN_NOTIFICATION: Enable/disable Server酱 notifications (default: true)
|
|
* - HAPI_LISTEN_HOST: Host/IP to bind the HTTP service (default: 127.0.0.1)
|
|
* - HAPI_LISTEN_PORT: Port for HTTP service (default: 3006)
|
|
* - HAPI_PUBLIC_URL: Public URL for external access (e.g., Telegram Mini App)
|
|
* - CORS_ORIGINS: Comma-separated CORS origins
|
|
* - HAPI_RELAY_API: Relay API domain for tunwg (default: relay.hapi.run)
|
|
* - HAPI_RELAY_AUTH: Relay auth key override (default: per-hub key issued by the relay)
|
|
* - HAPI_RELAY_FORCE_TCP: Force TCP relay mode when UDP is unavailable (true/1)
|
|
* - VAPID_SUBJECT: Contact email or URL for Web Push (defaults to mailto:admin@hapi.run)
|
|
* - HAPI_HOME: Data directory (default: ~/.hapi)
|
|
* - DB_PATH: SQLite database path (default: {HAPI_HOME}/hapi.db)
|
|
*/
|
|
|
|
import { existsSync, mkdirSync } from 'node:fs'
|
|
import { homedir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { getOrCreateCliApiToken } from './config/cliApiToken'
|
|
import { getSettingsFile } from './config/settings'
|
|
import { loadServerSettings, type ServerSettings, type ServerSettingsResult } from './config/serverSettings'
|
|
|
|
export type ConfigSource = 'env' | 'file' | 'default'
|
|
|
|
export interface ConfigSources {
|
|
telegramBotToken: ConfigSource
|
|
telegramNotification: ConfigSource
|
|
serverChanSendKey: ConfigSource
|
|
serverChanNotification: ConfigSource
|
|
listenHost: ConfigSource
|
|
listenPort: ConfigSource
|
|
publicUrl: ConfigSource
|
|
corsOrigins: ConfigSource
|
|
cliApiToken: 'env' | 'file' | 'generated'
|
|
}
|
|
|
|
class Configuration {
|
|
/** Telegram Bot API token */
|
|
public readonly telegramBotToken: string | null
|
|
|
|
/** Telegram bot enabled status (token present) */
|
|
public readonly telegramEnabled: boolean
|
|
|
|
/** Telegram notifications enabled */
|
|
public readonly telegramNotification: boolean
|
|
|
|
/** Server酱 SendKey/AppKey */
|
|
public readonly serverChanSendKey: string | null
|
|
|
|
/** Server酱 notifications enabled */
|
|
public readonly serverChanNotification: boolean
|
|
|
|
/** CLI auth token (shared secret) */
|
|
public cliApiToken: string
|
|
|
|
/** Source of CLI API token */
|
|
public cliApiTokenSource: 'env' | 'file' | 'generated' | ''
|
|
|
|
/** Whether CLI API token was newly generated (for first-run display) */
|
|
public cliApiTokenIsNew: boolean
|
|
|
|
/** Path to settings.json file */
|
|
public readonly settingsFile: string
|
|
|
|
/** Data directory for credentials and state */
|
|
public readonly dataDir: string
|
|
|
|
/** SQLite DB path */
|
|
public readonly dbPath: string
|
|
|
|
/** Port for the HTTP service */
|
|
public readonly listenPort: number
|
|
|
|
/** Host/IP to bind the HTTP service to */
|
|
public readonly listenHost: string
|
|
|
|
/** Public URL for external access (e.g., Telegram Mini App) */
|
|
public readonly publicUrl: string
|
|
|
|
/** Allowed CORS origins for Mini App + Socket.IO (comma-separated env override) */
|
|
public readonly corsOrigins: string[]
|
|
|
|
/** Sources of each configuration value */
|
|
public readonly sources: ConfigSources
|
|
|
|
/** Private constructor - use createConfiguration() instead */
|
|
private constructor(
|
|
dataDir: string,
|
|
dbPath: string,
|
|
serverSettings: ServerSettings,
|
|
sources: ServerSettingsResult['sources']
|
|
) {
|
|
this.dataDir = dataDir
|
|
this.dbPath = dbPath
|
|
this.settingsFile = getSettingsFile(dataDir)
|
|
|
|
// Apply server settings
|
|
this.telegramBotToken = serverSettings.telegramBotToken
|
|
this.telegramEnabled = Boolean(this.telegramBotToken)
|
|
this.telegramNotification = serverSettings.telegramNotification
|
|
this.serverChanSendKey = serverSettings.serverChanSendKey
|
|
this.serverChanNotification = serverSettings.serverChanNotification
|
|
this.listenHost = serverSettings.listenHost
|
|
this.listenPort = serverSettings.listenPort
|
|
this.publicUrl = serverSettings.publicUrl
|
|
this.corsOrigins = serverSettings.corsOrigins
|
|
|
|
// CLI API token - will be set by _setCliApiToken() before create() returns
|
|
this.cliApiToken = ''
|
|
this.cliApiTokenSource = ''
|
|
this.cliApiTokenIsNew = false
|
|
|
|
// Store sources for logging (cliApiToken will be set by _setCliApiToken)
|
|
this.sources = {
|
|
...sources,
|
|
} as ConfigSources
|
|
|
|
// Ensure data directory exists
|
|
if (!existsSync(this.dataDir)) {
|
|
mkdirSync(this.dataDir, { recursive: true })
|
|
}
|
|
}
|
|
|
|
/** Create configuration asynchronously */
|
|
static async create(): Promise<Configuration> {
|
|
// 1. Determine data directory (env only - not persisted)
|
|
const dataDir = process.env.HAPI_HOME
|
|
? process.env.HAPI_HOME.replace(/^~/, homedir())
|
|
: join(homedir(), '.hapi')
|
|
|
|
// Ensure data directory exists before loading settings
|
|
if (!existsSync(dataDir)) {
|
|
mkdirSync(dataDir, { recursive: true })
|
|
}
|
|
|
|
// 2. Determine DB path (env only - not persisted)
|
|
const dbPath = process.env.DB_PATH
|
|
? process.env.DB_PATH.replace(/^~/, homedir())
|
|
: join(dataDir, 'hapi.db')
|
|
|
|
// 3. Load hub settings (with persistence)
|
|
const settingsResult = await loadServerSettings(dataDir)
|
|
|
|
if (settingsResult.savedToFile) {
|
|
console.log(`[Hub] Configuration saved to ${getSettingsFile(dataDir)}`)
|
|
}
|
|
|
|
// 4. Create configuration instance
|
|
const config = new Configuration(
|
|
dataDir,
|
|
dbPath,
|
|
settingsResult.settings,
|
|
settingsResult.sources
|
|
)
|
|
|
|
// 5. Load CLI API token
|
|
const tokenResult = await getOrCreateCliApiToken(dataDir)
|
|
config._setCliApiToken(tokenResult.token, tokenResult.source, tokenResult.isNew)
|
|
|
|
return config
|
|
}
|
|
|
|
/** Set CLI API token (called during async initialization) */
|
|
_setCliApiToken(token: string, source: 'env' | 'file' | 'generated', isNew: boolean): void {
|
|
this.cliApiToken = token
|
|
this.cliApiTokenSource = source
|
|
this.cliApiTokenIsNew = isNew
|
|
;(this.sources as { cliApiToken: string }).cliApiToken = source
|
|
}
|
|
}
|
|
|
|
// Singleton instance (set by createConfiguration)
|
|
let _configuration: Configuration | null = null
|
|
|
|
/**
|
|
* Create and initialize configuration asynchronously.
|
|
* Must be called once at startup before getConfiguration() can be used.
|
|
*/
|
|
export async function createConfiguration(): Promise<Configuration> {
|
|
if (_configuration) {
|
|
return _configuration
|
|
}
|
|
_configuration = await Configuration.create()
|
|
return _configuration
|
|
}
|
|
|
|
/**
|
|
* Get the initialized configuration.
|
|
* Throws if createConfiguration() has not been called yet.
|
|
*/
|
|
export function getConfiguration(): Configuration {
|
|
if (!_configuration) {
|
|
throw new Error('Configuration not initialized. Call createConfiguration() first.')
|
|
}
|
|
return _configuration
|
|
}
|