config.go raw
1 package main
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/adrg/xdg"
12 )
13
14 // ConfigFile is the JSON structure for persistent configuration.
15 type ConfigFile struct {
16 DBBackend string `json:"db_backend,omitempty"`
17 DBBinary string `json:"db_binary,omitempty"`
18 RelayBinary string `json:"relay_binary,omitempty"`
19 ACLBinary string `json:"acl_binary,omitempty"`
20 DBListen string `json:"db_listen,omitempty"`
21 ACLListen string `json:"acl_listen,omitempty"`
22 ACLEnabled *bool `json:"acl_enabled,omitempty"`
23 ACLMode string `json:"acl_mode,omitempty"`
24 DataDir string `json:"data_dir,omitempty"`
25 LogLevel string `json:"log_level,omitempty"`
26 AdminPort *int `json:"admin_port,omitempty"`
27 AdminOwners []string `json:"admin_owners,omitempty"`
28 BinDir string `json:"bin_dir,omitempty"`
29 RelayPort *int `json:"relay_port,omitempty"`
30 RelayHost string `json:"relay_host,omitempty"`
31 TLSDomains string `json:"tls_domains,omitempty"`
32 AuthToWrite *bool `json:"auth_to_write,omitempty"`
33 AuthRequired *bool `json:"auth_required,omitempty"`
34
35 // Sync services
36 DistributedSyncEnabled *bool `json:"distributed_sync_enabled,omitempty"`
37 ClusterSyncEnabled *bool `json:"cluster_sync_enabled,omitempty"`
38 RelayGroupEnabled *bool `json:"relay_group_enabled,omitempty"`
39 NegentropyEnabled *bool `json:"negentropy_enabled,omitempty"`
40 NegentropyBinary string `json:"negentropy_binary,omitempty"`
41 NegentropyListen string `json:"negentropy_listen,omitempty"`
42
43 // Certificate service
44 CertsEnabled *bool `json:"certs_enabled,omitempty"`
45 CertsBinary string `json:"certs_binary,omitempty"`
46
47 // Bitcoin node (nits)
48 NitsEnabled *bool `json:"nits_enabled,omitempty"`
49 NitsBinary string `json:"nits_binary,omitempty"`
50 NitsShimBinary string `json:"nits_shim_binary,omitempty"`
51 NitsListen string `json:"nits_listen,omitempty"`
52 NitsRPCPort *int `json:"nits_rpc_port,omitempty"`
53 NitsDataDir string `json:"nits_data_dir,omitempty"`
54 NitsPruneMB *int `json:"nits_prune_mb,omitempty"`
55 NitsNetwork string `json:"nits_network,omitempty"`
56
57 // Lightning node (luk)
58 LukEnabled *bool `json:"luk_enabled,omitempty"`
59 LukBinary string `json:"luk_binary,omitempty"`
60 LukDataDir string `json:"luk_data_dir,omitempty"`
61 LukRPCListen string `json:"luk_rpc_listen,omitempty"`
62 LukPeerListen string `json:"luk_peer_listen,omitempty"`
63
64 // Wallet (strela)
65 StrelaEnabled *bool `json:"strela_enabled,omitempty"`
66 StrelaBinary string `json:"strela_binary,omitempty"`
67 StrelaPort *int `json:"strela_port,omitempty"`
68 StrelaDataDir string `json:"strela_data_dir,omitempty"`
69 }
70
71 // configFilePath returns the path to the config file.
72 func configFilePath() string {
73 return filepath.Join(xdg.ConfigHome, "orly", "launcher.json")
74 }
75
76 // loadConfigFile loads configuration from the JSON file if it exists.
77 func loadConfigFile() (*ConfigFile, error) {
78 path := configFilePath()
79 data, err := os.ReadFile(path)
80 if err != nil {
81 if os.IsNotExist(err) {
82 return &ConfigFile{}, nil
83 }
84 return nil, err
85 }
86
87 var cf ConfigFile
88 if err := json.Unmarshal(data, &cf); err != nil {
89 return nil, err
90 }
91 return &cf, nil
92 }
93
94 // SaveConfigFile saves the configuration to the JSON file.
95 func SaveConfigFile(cf *ConfigFile) error {
96 path := configFilePath()
97
98 // Ensure directory exists
99 dir := filepath.Dir(path)
100 if err := os.MkdirAll(dir, 0755); err != nil {
101 return err
102 }
103
104 data, err := json.MarshalIndent(cf, "", " ")
105 if err != nil {
106 return err
107 }
108
109 return os.WriteFile(path, data, 0644)
110 }
111
112 // ConfigToFile converts a Config to a ConfigFile for persistence.
113 func ConfigToFile(cfg *Config) *ConfigFile {
114 return &ConfigFile{
115 DBBackend: cfg.DBBackend,
116 DBBinary: cfg.DBBinary,
117 RelayBinary: cfg.RelayBinary,
118 ACLBinary: cfg.ACLBinary,
119 DBListen: cfg.DBListen,
120 ACLListen: cfg.ACLListen,
121 ACLEnabled: &cfg.ACLEnabled,
122 ACLMode: cfg.ACLMode,
123 DataDir: cfg.DataDir,
124 LogLevel: cfg.LogLevel,
125 AdminPort: &cfg.AdminPort,
126 AdminOwners: cfg.AdminOwners,
127 BinDir: cfg.BinDir,
128 DistributedSyncEnabled: &cfg.DistributedSyncEnabled,
129 ClusterSyncEnabled: &cfg.ClusterSyncEnabled,
130 RelayGroupEnabled: &cfg.RelayGroupEnabled,
131 NegentropyEnabled: &cfg.NegentropyEnabled,
132 NegentropyBinary: cfg.NegentropyBinary,
133 NegentropyListen: cfg.NegentropyListen,
134 CertsEnabled: &cfg.CertsEnabled,
135 CertsBinary: cfg.CertsBinary,
136 NitsEnabled: &cfg.NitsEnabled,
137 NitsBinary: cfg.NitsBinary,
138 NitsShimBinary: cfg.NitsShimBinary,
139 NitsListen: cfg.NitsListen,
140 NitsRPCPort: &cfg.NitsRPCPort,
141 NitsDataDir: cfg.NitsDataDir,
142 NitsPruneMB: &cfg.NitsPruneMB,
143 NitsNetwork: cfg.NitsNetwork,
144 LukEnabled: &cfg.LukEnabled,
145 LukBinary: cfg.LukBinary,
146 LukDataDir: cfg.LukDataDir,
147 LukRPCListen: cfg.LukRPCListen,
148 LukPeerListen: cfg.LukPeerListen,
149 StrelaEnabled: &cfg.StrelaEnabled,
150 StrelaBinary: cfg.StrelaBinary,
151 StrelaPort: &cfg.StrelaPort,
152 StrelaDataDir: cfg.StrelaDataDir,
153 }
154 }
155
156 // Config holds the launcher configuration.
157 type Config struct {
158 // DBBackend is the database backend: badger or neo4j
159 DBBackend string
160
161 // DBBinary is the path to the database server binary (computed from DBBackend if not set)
162 DBBinary string
163
164 // RelayBinary is the path to the orly binary
165 RelayBinary string
166
167 // ACLBinary is the path to the ACL server binary (computed from ACLMode if not set)
168 ACLBinary string
169
170 // DBListen is the address the database server listens on
171 DBListen string
172
173 // ACLListen is the address the ACL server listens on
174 ACLListen string
175
176 // ACLEnabled controls whether to run the ACL server as a separate process
177 // When false, the relay runs in open mode (no ACL restrictions)
178 ACLEnabled bool
179
180 // ACLMode is the ACL mode: follows, managed, curation
181 // Determines which ACL binary to use when ACLEnabled is true
182 ACLMode string
183
184 // DBReadyTimeout is how long to wait for the database to be ready
185 DBReadyTimeout time.Duration
186
187 // ACLReadyTimeout is how long to wait for the ACL server to be ready
188 ACLReadyTimeout time.Duration
189
190 // StopTimeout is how long to wait for processes to stop gracefully
191 StopTimeout time.Duration
192
193 // DataDir is the data directory to pass to orly-db
194 DataDir string
195
196 // LogLevel is the log level to use for all processes
197 LogLevel string
198
199 // Sync service configuration
200 // DistributedSyncEnabled enables the distributed sync service
201 DistributedSyncEnabled bool
202 // DistributedSyncBinary is the path to the distributed sync binary
203 DistributedSyncBinary string
204 // DistributedSyncListen is the gRPC listen address for distributed sync
205 DistributedSyncListen string
206
207 // ClusterSyncEnabled enables the cluster sync service
208 ClusterSyncEnabled bool
209 // ClusterSyncBinary is the path to the cluster sync binary
210 ClusterSyncBinary string
211 // ClusterSyncListen is the gRPC listen address for cluster sync
212 ClusterSyncListen string
213
214 // RelayGroupEnabled enables the relay group service
215 RelayGroupEnabled bool
216 // RelayGroupBinary is the path to the relay group binary
217 RelayGroupBinary string
218 // RelayGroupListen is the gRPC listen address for relay group
219 RelayGroupListen string
220
221 // NegentropyEnabled enables the negentropy sync service
222 NegentropyEnabled bool
223 // NegentropyBinary is the path to the negentropy sync binary
224 NegentropyBinary string
225 // NegentropyListen is the gRPC listen address for negentropy
226 NegentropyListen string
227
228 // SyncReadyTimeout is how long to wait for sync services to be ready
229 SyncReadyTimeout time.Duration
230
231 // Certificate service configuration
232 // CertsEnabled enables the certificate service
233 CertsEnabled bool
234 // CertsBinary is the path to the certificate service binary
235 CertsBinary string
236
237 // Bitcoin node (nits) configuration
238 // NitsEnabled enables the Bitcoin node manager
239 NitsEnabled bool
240 // NitsBinary is the path to the bitcoind binary
241 NitsBinary string
242 // NitsShimBinary is the path to the orly-nits gRPC shim binary
243 NitsShimBinary string
244 // NitsListen is the gRPC listen address for the nits shim
245 NitsListen string
246 // NitsRPCPort is the JSON-RPC port for bitcoind
247 NitsRPCPort int
248 // NitsDataDir is the data directory for bitcoind
249 NitsDataDir string
250 // NitsPruneMB is the prune target in MB (0 = no pruning)
251 NitsPruneMB int
252 // NitsNetwork is the bitcoin network: mainnet, testnet, signet, regtest
253 NitsNetwork string
254 // NitsReadyTimeout is how long to wait for bitcoind to respond
255 NitsReadyTimeout time.Duration
256
257 // Lightning node (luk) configuration
258 // LukEnabled enables the Lightning node
259 LukEnabled bool
260 // LukBinary is the path to the luk binary
261 LukBinary string
262 // LukDataDir is the data directory for luk
263 LukDataDir string
264 // LukRPCListen is the gRPC listen address for luk
265 LukRPCListen string
266 // LukPeerListen is the P2P listen address for luk
267 LukPeerListen string
268 // LukReadyTimeout is how long to wait for luk gRPC to be reachable
269 LukReadyTimeout time.Duration
270
271 // Wallet (strela) configuration
272 // StrelaEnabled enables the wallet web UI
273 StrelaEnabled bool
274 // StrelaBinary is the path to the strela binary
275 StrelaBinary string
276 // StrelaPort is the HTTP port for strela
277 StrelaPort int
278 // StrelaDataDir is the work directory for strela
279 StrelaDataDir string
280 // StrelaReadyTimeout is how long to wait for strela HTTP to be reachable
281 StrelaReadyTimeout time.Duration
282
283 // ServicesEnabled controls whether to start the DB, relay, and other services
284 // When false, only the admin UI runs (useful for initial setup/updates)
285 ServicesEnabled bool
286
287 // Admin UI configuration
288 // AdminEnabled controls whether to run the admin HTTP server
289 AdminEnabled bool
290 // AdminPort is the port for the admin HTTP server
291 AdminPort int
292 // AdminOwners is a list of pubkeys (hex) allowed to access the admin UI
293 AdminOwners []string
294 // BinDir is the directory for versioned binary management
295 BinDir string
296 }
297
298 func loadConfig() (*Config, error) {
299 // Load config file first (provides defaults)
300 cf, err := loadConfigFile()
301 if err != nil {
302 // Log but don't fail - env vars are still valid
303 cf = &ConfigFile{}
304 }
305
306 // Get backend and mode - file first, then env
307 dbBackend := stringOr(cf.DBBackend, getEnvOrDefault("ORLY_LAUNCHER_DB_BACKEND", "badger"))
308 aclMode := stringOr(cf.ACLMode, getEnvOrDefault("ORLY_ACL_MODE", "follows"))
309
310 // Compute default binary names based on backend/mode
311 defaultDBBinary := "orly-db-" + dbBackend
312 defaultACLBinary := "orly-acl-" + aclMode
313
314 // Parse admin owners - env takes precedence, then file
315 envOwners := getEnvOrDefault("ORLY_LAUNCHER_OWNERS", "")
316 var adminOwners []string
317 if envOwners != "" {
318 adminOwners = parseOwnersList(envOwners)
319 } else if len(cf.AdminOwners) > 0 {
320 adminOwners = cf.AdminOwners
321 }
322
323 cfg := &Config{
324 DBBackend: dbBackend,
325 DBBinary: envOrFileOrDefault("ORLY_LAUNCHER_DB_BINARY", cf.DBBinary, defaultDBBinary),
326 RelayBinary: envOrFileOrDefault("ORLY_LAUNCHER_RELAY_BINARY", cf.RelayBinary, "orly"),
327 ACLBinary: envOrFileOrDefault("ORLY_LAUNCHER_ACL_BINARY", cf.ACLBinary, defaultACLBinary),
328 DBListen: envOrFileOrDefault("ORLY_LAUNCHER_DB_LISTEN", cf.DBListen, "127.0.0.1:50051"),
329 ACLListen: envOrFileOrDefault("ORLY_LAUNCHER_ACL_LISTEN", cf.ACLListen, "127.0.0.1:50052"),
330 ACLEnabled: boolEnvOrFile("ORLY_LAUNCHER_ACL_ENABLED", cf.ACLEnabled, false),
331 ACLMode: aclMode,
332 DBReadyTimeout: parseDuration("ORLY_LAUNCHER_DB_READY_TIMEOUT", 30*time.Second),
333 ACLReadyTimeout: parseDuration("ORLY_LAUNCHER_ACL_READY_TIMEOUT", 120*time.Second),
334 StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second),
335 DataDir: envOrFileOrDefault("ORLY_DATA_DIR", cf.DataDir, filepath.Join(xdg.DataHome, "ORLY")),
336 LogLevel: envOrFileOrDefault("ORLY_LOG_LEVEL", cf.LogLevel, "info"),
337
338 // Sync services configuration
339 DistributedSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED", cf.DistributedSyncEnabled, false),
340 DistributedSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY", "orly-sync-distributed"),
341 DistributedSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN", "127.0.0.1:50061"),
342
343 ClusterSyncEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED", cf.ClusterSyncEnabled, false),
344 ClusterSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_BINARY", "orly-sync-cluster"),
345 ClusterSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN", "127.0.0.1:50062"),
346
347 RelayGroupEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED", cf.RelayGroupEnabled, false),
348 RelayGroupBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY", "orly-sync-relaygroup"),
349 RelayGroupListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN", "127.0.0.1:50063"),
350
351 NegentropyEnabled: boolEnvOrFile("ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED", cf.NegentropyEnabled, false),
352 NegentropyBinary: envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY", cf.NegentropyBinary, "orly-sync-negentropy"),
353 NegentropyListen: envOrFileOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", cf.NegentropyListen, "127.0.0.1:50064"),
354
355 SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second),
356
357 // Certificate service configuration
358 CertsEnabled: boolEnvOrFile("ORLY_LAUNCHER_CERTS_ENABLED", cf.CertsEnabled, false),
359 CertsBinary: envOrFileOrDefault("ORLY_LAUNCHER_CERTS_BINARY", cf.CertsBinary, "orly-certs"),
360
361 // Bitcoin node (nits) configuration
362 NitsEnabled: boolEnvOrFile("ORLY_LAUNCHER_NITS_ENABLED", cf.NitsEnabled, false),
363 NitsBinary: envOrFileOrDefault("ORLY_LAUNCHER_NITS_BINARY", cf.NitsBinary, "bitcoind"),
364 NitsShimBinary: envOrFileOrDefault("ORLY_LAUNCHER_NITS_SHIM_BINARY", cf.NitsShimBinary, "orly-nits"),
365 NitsListen: envOrFileOrDefault("ORLY_LAUNCHER_NITS_LISTEN", cf.NitsListen, "127.0.0.1:50070"),
366 NitsRPCPort: intEnvOrFile("ORLY_LAUNCHER_NITS_RPC_PORT", cf.NitsRPCPort, 8332),
367 NitsDataDir: envOrFileOrDefault("ORLY_LAUNCHER_NITS_DATA_DIR", cf.NitsDataDir, filepath.Join(xdg.DataHome, "orly", "nits")),
368 NitsPruneMB: intEnvOrFile("ORLY_LAUNCHER_NITS_PRUNE_MB", cf.NitsPruneMB, 2048),
369 NitsNetwork: envOrFileOrDefault("ORLY_LAUNCHER_NITS_NETWORK", cf.NitsNetwork, "mainnet"),
370 NitsReadyTimeout: parseDuration("ORLY_LAUNCHER_NITS_READY_TIMEOUT", 120*time.Second),
371
372 // Lightning node (luk) configuration
373 LukEnabled: boolEnvOrFile("ORLY_LAUNCHER_LUK_ENABLED", cf.LukEnabled, false),
374 LukBinary: envOrFileOrDefault("ORLY_LAUNCHER_LUK_BINARY", cf.LukBinary, "luk"),
375 LukDataDir: envOrFileOrDefault("ORLY_LAUNCHER_LUK_DATA_DIR", cf.LukDataDir, filepath.Join(xdg.DataHome, "orly", "luk")),
376 LukRPCListen: envOrFileOrDefault("ORLY_LAUNCHER_LUK_RPC_LISTEN", cf.LukRPCListen, "127.0.0.1:10009"),
377 LukPeerListen: envOrFileOrDefault("ORLY_LAUNCHER_LUK_PEER_LISTEN", cf.LukPeerListen, "0.0.0.0:9735"),
378 LukReadyTimeout: parseDuration("ORLY_LAUNCHER_LUK_READY_TIMEOUT", 60*time.Second),
379
380 // Wallet (strela) configuration
381 StrelaEnabled: boolEnvOrFile("ORLY_LAUNCHER_STRELA_ENABLED", cf.StrelaEnabled, false),
382 StrelaBinary: envOrFileOrDefault("ORLY_LAUNCHER_STRELA_BINARY", cf.StrelaBinary, "strela"),
383 StrelaPort: intEnvOrFile("ORLY_LAUNCHER_STRELA_PORT", cf.StrelaPort, 8090),
384 StrelaDataDir: envOrFileOrDefault("ORLY_LAUNCHER_STRELA_DATA_DIR", cf.StrelaDataDir, filepath.Join(xdg.DataHome, "orly", "strela")),
385 StrelaReadyTimeout: parseDuration("ORLY_LAUNCHER_STRELA_READY_TIMEOUT", 30*time.Second),
386
387 // Services enabled (default true for backwards compatibility)
388 ServicesEnabled: getEnvOrDefault("ORLY_LAUNCHER_SERVICES_ENABLED", "true") == "true",
389
390 // Admin UI configuration
391 AdminEnabled: getEnvOrDefault("ORLY_LAUNCHER_ADMIN_ENABLED", "true") == "true",
392 AdminPort: intEnvOrFile("ORLY_LAUNCHER_ADMIN_PORT", cf.AdminPort, 8080),
393 AdminOwners: adminOwners,
394 BinDir: envOrFileOrDefault("ORLY_LAUNCHER_BIN_DIR", cf.BinDir, filepath.Join(xdg.DataHome, "orly", "bin")),
395 }
396
397 return cfg, nil
398 }
399
400 // stringOr returns the first non-empty string.
401 func stringOr(a, b string) string {
402 if a != "" {
403 return a
404 }
405 return b
406 }
407
408 // envOrFileOrDefault returns env var if set, then file value if set, then default.
409 func envOrFileOrDefault(envKey, fileValue, defaultValue string) string {
410 if v := os.Getenv(envKey); v != "" {
411 return v
412 }
413 if fileValue != "" {
414 return fileValue
415 }
416 return defaultValue
417 }
418
419 // boolEnvOrFile returns env var if set, then file value if set, then default.
420 func boolEnvOrFile(envKey string, fileValue *bool, defaultValue bool) bool {
421 if v := os.Getenv(envKey); v != "" {
422 return v == "true"
423 }
424 if fileValue != nil {
425 return *fileValue
426 }
427 return defaultValue
428 }
429
430 // intEnvOrFile returns env var if set, then file value if set, then default.
431 func intEnvOrFile(envKey string, fileValue *int, defaultValue int) int {
432 if v := os.Getenv(envKey); v != "" {
433 if i, err := strconv.Atoi(v); err == nil {
434 return i
435 }
436 }
437 if fileValue != nil {
438 return *fileValue
439 }
440 return defaultValue
441 }
442
443 func parseOwnersList(s string) []string {
444 if s == "" {
445 return nil
446 }
447 parts := strings.Split(s, ",")
448 var owners []string
449 for _, p := range parts {
450 p = strings.TrimSpace(p)
451 if p != "" {
452 owners = append(owners, p)
453 }
454 }
455 return owners
456 }
457
458 func parseInt(key string, defaultValue int) int {
459 if v := os.Getenv(key); v != "" {
460 if i, err := strconv.Atoi(v); err == nil {
461 return i
462 }
463 }
464 return defaultValue
465 }
466
467 func getEnvOrDefault(key, defaultValue string) string {
468 if v := os.Getenv(key); v != "" {
469 return v
470 }
471 return defaultValue
472 }
473
474 func parseDuration(key string, defaultValue time.Duration) time.Duration {
475 if v := os.Getenv(key); v != "" {
476 if d, err := time.ParseDuration(v); err == nil {
477 return d
478 }
479 }
480 return defaultValue
481 }
482