124 lines
3.5 KiB
Go
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()
|
||
|
|
}
|