Files
awg-profiler-golang/registry.go
T
2026-07-18 10:02:43 +03:00

124 lines
3.5 KiB
Go

package main
import (
"encoding/json"
"os"
"sort"
"time"
)
// Client is one entry in the JSON registry. The field tags reproduce the exact
// object shape the bash/jq version wrote, so registries are interchangeable.
type Client struct {
ID int64 `json:"id"`
Name string `json:"name"`
IP string `json:"ip"`
PublicKey string `json:"public_key"`
PrivateKey string `json:"private_key"`
PSKKey string `json:"psk_key"`
IsEnabled string `json:"is_enabled"`
CreatedAt int64 `json:"created_at"`
// Comment is a free-form note attached to the profile via the web UI. It is
// registry-only (never written into the .conf) and omitted when empty so
// registries stay byte-compatible with the bash/jq version until a note is set.
Comment string `json:"comment,omitempty"`
}
// initStorage creates the data/client directories and an empty registry,
// validating any existing registry JSON (bash: jq empty). Both directories
// hold secret material (the registry's private_key field, client .conf/.png
// files) so they are created owner-only.
func initStorage() {
if err := os.MkdirAll(dataDir, 0700); err != nil {
die("Failed to create data dir: %v", err)
}
if err := os.MkdirAll(clientDir, 0700); err != nil {
die("Failed to create client dir: %v", err)
}
if _, err := os.Stat(registryFile); os.IsNotExist(err) {
if err := os.WriteFile(registryFile, []byte("[]"), 0644); err != nil {
die("Failed to initialise registry: %v", err)
}
}
// Validate JSON (equivalent to `jq empty`).
loadRegistry()
}
// loadRegistry reads and parses the registry, aborting on invalid JSON.
func loadRegistry() []Client {
data, err := os.ReadFile(registryFile)
if err != nil {
die("Failed to read registry: %v", err)
}
var clients []Client
if err := json.Unmarshal(data, &clients); err != nil {
die("Registry JSON is invalid")
}
return clients
}
// saveRegistry writes the registry atomically (temp file + rename), matching
// the bash mktemp/mv pattern, then re-validates it.
func saveRegistry(clients []Client) {
data, err := json.MarshalIndent(clients, "", " ")
if err != nil {
die("Failed to update registry: %v", err)
}
tmp, err := os.CreateTemp(dataDir, "registry-*.tmp")
if err != nil {
die("Failed to update registry: %v", err)
}
tmpName := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmpName)
die("Failed to update registry: %v", err)
}
tmp.Close()
if err := os.Rename(tmpName, registryFile); err != nil {
os.Remove(tmpName)
die("Failed to update registry: %v", err)
}
// Re-validate (bash: jq empty after write).
loadRegistry()
}
// getNextID returns max(id)+1, or 1 for an empty registry.
func getNextID(clients []Client) int64 {
if len(clients) == 0 {
return 1
}
var max int64
for _, c := range clients {
if c.ID > max {
max = c.ID
}
}
return max + 1
}
// getLastIP returns the IP of the highest-id client (empty registry → "").
func getLastIP(clients []Client) string {
if len(clients) == 0 {
return ""
}
sorted := append([]Client(nil), clients...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].ID < sorted[j].ID })
return sorted[len(sorted)-1].IP
}
// findClient returns a pointer to the client with the given id, or nil.
func findClient(clients []Client, id int64) *Client {
for i := range clients {
if clients[i].ID == id {
return &clients[i]
}
}
return nil
}
// nowUnix returns the current Unix timestamp (bash: now|floor).
func nowUnix() int64 {
return time.Now().Unix()
}