158 lines
3.4 KiB
Go
158 lines
3.4 KiB
Go
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
|
|
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,
|
|
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 (a *Agent) health(w http.ResponseWriter, _ *http.Request) {
|
|
a.mu.RLock()
|
|
defer a.mu.RUnlock()
|
|
protocols := []string{}
|
|
if raw, ok := a.config["protocols"].([]any); ok {
|
|
for _, p := range raw {
|
|
if s, ok := p.(string); ok {
|
|
protocols = append(protocols, s)
|
|
}
|
|
}
|
|
}
|
|
writeJSON(w, map[string]any{
|
|
"ok": true,
|
|
"name": a.name,
|
|
"version": a.version,
|
|
"uptime_sec": int64(time.Since(a.startedAt).Seconds()),
|
|
"protocols": protocols,
|
|
})
|
|
}
|
|
|
|
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("/var/lib/vpn-node/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)
|
|
}
|