package main import ( "encoding/json" "log" "net/http" "os" "path/filepath" "strings" "sync" "time" ) const defaultVersion = "0.1.0" type Agent struct { name string secret string version string dataDir string startedAt time.Time mu sync.RWMutex config map[string]any } func main() { port := env("NODE_PORT", "2222") secret := os.Getenv("SECRET_KEY") if secret == "" { log.Fatal("SECRET_KEY is required") } name := env("NODE_NAME", "vpn-node") version := env("AGENT_VERSION", defaultVersion) dataDir := env("DATA_DIR", "/var/lib/vpn-node") _ = os.MkdirAll(dataDir, 0o755) a := &Agent{ name: name, secret: secret, version: version, dataDir: dataDir, startedAt: time.Now().UTC(), config: map[string]any{}, } a.loadConfig(filepath.Join(dataDir, "config.json")) mux := http.NewServeMux() mux.HandleFunc("/health", a.auth(a.health)) mux.HandleFunc("/api/v1/config", a.auth(a.configHandler)) mux.HandleFunc("/api/v1/status", a.auth(a.status)) addr := ":" + port log.Printf("vpn-node %s listening on %s name=%s", version, addr, name) if err := http.ListenAndServe(addr, mux); err != nil { log.Fatal(err) } } func env(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func (a *Agent) auth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h := r.Header.Get("Authorization") token := strings.TrimPrefix(h, "Bearer ") if token == "" { token = r.Header.Get("X-Node-Secret") } if token != a.secret { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next(w, r) } } func stringList(cfg map[string]any, key string) []string { out := []string{} raw, ok := cfg[key] if !ok { return out } switch v := raw.(type) { case []any: for _, item := range v { if s, ok := item.(string); ok && s != "" { out = append(out, s) } } case []string: out = append(out, v...) } return out } func (a *Agent) health(w http.ResponseWriter, _ *http.Request) { a.mu.RLock() defer a.mu.RUnlock() enabled := stringList(a.config, "protocols_enabled") if len(enabled) == 0 { enabled = stringList(a.config, "protocols") } installed := stringList(a.config, "protocols_installed") if len(installed) == 0 { installed = append([]string{}, enabled...) } writeJSON(w, map[string]any{ "ok": true, "name": a.name, "version": a.version, "uptime_sec": int64(time.Since(a.startedAt).Seconds()), "protocols": enabled, "protocols_installed": installed, "protocols_enabled": enabled, }) } func (a *Agent) status(w http.ResponseWriter, _ *http.Request) { a.mu.RLock() defer a.mu.RUnlock() writeJSON(w, map[string]any{ "ok": true, "name": a.name, "version": a.version, "config": a.config, }) } func (a *Agent) configHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: a.mu.RLock() defer a.mu.RUnlock() writeJSON(w, a.config) case http.MethodPost: var cfg map[string]any if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } a.mu.Lock() a.config = cfg a.mu.Unlock() _ = a.saveConfig(filepath.Join(a.dataDir, "config.json")) writeJSON(w, map[string]any{"ok": true}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func (a *Agent) loadConfig(path string) { b, err := os.ReadFile(path) if err != nil { return } var cfg map[string]any if json.Unmarshal(b, &cfg) == nil { a.config = cfg } } func (a *Agent) saveConfig(path string) error { a.mu.RLock() defer a.mu.RUnlock() b, err := json.MarshalIndent(a.config, "", " ") if err != nil { return err } return os.WriteFile(path, b, 0o600) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) }