124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// AgentConfig holds user-editable settings for the agent.
|
|
type AgentConfig struct {
|
|
Server string `json:"server"`
|
|
HeadscaleURL string `json:"headscale_url"`
|
|
HeadscaleAuthKey string `json:"headscale_auth_key"`
|
|
NodeID string `json:"node_id"`
|
|
DataDir string `json:"data_dir"`
|
|
// ProxyURL is an optional HTTP(S) proxy used for all outbound agent traffic
|
|
// (WebSocket, update checks, downloads).
|
|
ProxyURL string `json:"proxy_url,omitempty"`
|
|
// ProxyMode controls how the proxy is used:
|
|
// - "disabled" : never use the proxy.
|
|
// - "auto" : the agent tries direct connections first and falls back to
|
|
// the proxy after a few failures (useful when moving between
|
|
// home network and school network).
|
|
// - "enabled" : always use the proxy.
|
|
ProxyMode string `json:"proxy_mode,omitempty"`
|
|
}
|
|
|
|
const configFileName = "studioE5-config.json"
|
|
|
|
// defaultServerURL is the production WebSocket endpoint baked into the agent.
|
|
// It can be overridden by the config file for self-hosted or test setups.
|
|
const defaultServerURL = "wss://studioe5.edudeploy.com/api/websocket"
|
|
|
|
// uniqueNodeID returns a stable-ish unique identifier for this machine.
|
|
// It combines the hostname with a short random suffix so every install is distinct.
|
|
func uniqueNodeID() string {
|
|
h, err := os.Hostname()
|
|
if err != nil || h == "" {
|
|
h = "node"
|
|
}
|
|
b := make([]byte, 4)
|
|
if _, err := rand.Read(b); err == nil {
|
|
return fmt.Sprintf("%s-%s", h, hex.EncodeToString(b))
|
|
}
|
|
return fmt.Sprintf("%s-%d", h, os.Getpid())
|
|
}
|
|
|
|
// defaultConfig returns sensible defaults for a first run.
|
|
// The user only needs to provide an activation code; Headscale credentials are
|
|
// delivered by the server during activation.
|
|
func defaultConfig(dataDir string) *AgentConfig {
|
|
return &AgentConfig{
|
|
Server: defaultServerURL,
|
|
HeadscaleURL: "",
|
|
HeadscaleAuthKey: "",
|
|
NodeID: uniqueNodeID(),
|
|
DataDir: dataDir,
|
|
}
|
|
}
|
|
|
|
// mergeWithDefaults fills missing fields from disk with sensible defaults.
|
|
func mergeWithDefaults(cfg *AgentConfig, dataDir string) *AgentConfig {
|
|
defaults := defaultConfig(dataDir)
|
|
if cfg == nil {
|
|
return defaults
|
|
}
|
|
if cfg.Server == "" {
|
|
cfg.Server = defaults.Server
|
|
}
|
|
if cfg.NodeID == "" {
|
|
cfg.NodeID = defaults.NodeID
|
|
}
|
|
if cfg.DataDir == "" {
|
|
cfg.DataDir = defaults.DataDir
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// configPath returns the absolute path to the config file.
|
|
func configPath(dataDir string) string {
|
|
return filepath.Join(dataDir, configFileName)
|
|
}
|
|
|
|
// loadOrCreateConfig loads the config file. If it does not exist, it creates
|
|
// one with default values and returns it (the caller can then open the settings UI).
|
|
func loadOrCreateConfig(dataDir string) (*AgentConfig, bool, error) {
|
|
cp := configPath(dataDir)
|
|
|
|
if _, err := os.Stat(cp); err == nil {
|
|
data, err := os.ReadFile(cp)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
cfg := &AgentConfig{}
|
|
if err := json.Unmarshal(data, cfg); err != nil {
|
|
return nil, false, err
|
|
}
|
|
cfg = mergeWithDefaults(cfg, dataDir)
|
|
return cfg, false, nil
|
|
}
|
|
|
|
cfg := defaultConfig(dataDir)
|
|
if err := saveConfig(dataDir, cfg); err != nil {
|
|
return nil, true, err
|
|
}
|
|
return cfg, true, nil
|
|
}
|
|
|
|
// saveConfig writes the config file to disk.
|
|
func saveConfig(dataDir string, cfg *AgentConfig) error {
|
|
cp := configPath(dataDir)
|
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(cfg, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(cp, data, 0600)
|
|
}
|