Architecture: - Extract reconcileNetworking into internal/reconcile package with 7 consumer-side interfaces - Add locked modifyState helper to fix state.yaml read-modify-write race condition - Extract CrowdSec and VPN handler groups with interfaces documenting dependency surface - Replace raw map[string]any YAML manipulation with typed AddDHCPStaticLease/RemoveDHCPStaticLease - Extract Cloudflare API functions into cfClient struct, eliminating repeated auth boilerplate Complexity reduction: - haproxy.GenerateWithOpts: 50 → 6 (extracted 8 focused helpers) - reconcile.Reconcile: 42 → 11 (extracted buildRoutes, buildDNSEntries, writeHAProxyConfig) - AutheliaUpdateConfig: 25 → 10 (extracted enableAuthelia/disableAuthelia) Test coverage improvements: - reconcile: 4.5% → 56.8% (stub-based tests for route building, DNS entries, orchestration) - dnsfilter: 10.7% → 45.6% (Manager.Compile, AddList, ToggleList, custom entries) - config: 67.3% → 89.8% (DHCP static lease mutation tests)
171 lines
5.2 KiB
Go
171 lines
5.2 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/wild-cloud/wild-central/internal/config"
|
|
"github.com/wild-cloud/wild-central/internal/wireguard"
|
|
)
|
|
|
|
// VPNManager is the interface for VPN operations used by these handlers.
|
|
type VPNManager interface {
|
|
GetStatus() (*wireguard.Status, error)
|
|
GetConfig() (*wireguard.Config, error)
|
|
SaveConfig(cfg *wireguard.Config) error
|
|
GetPublicKey() string
|
|
GenerateKeypair() error
|
|
Apply() error
|
|
ListPeers() ([]*wireguard.Peer, error)
|
|
AddPeer(name string) (*wireguard.Peer, error)
|
|
GetPeer(id string) (*wireguard.Peer, error)
|
|
DeletePeer(id string) error
|
|
GeneratePeerConfigText(peerID string) (string, error)
|
|
}
|
|
|
|
// vpnHandlers groups VPN HTTP handlers with their dependencies.
|
|
type vpnHandlers struct {
|
|
vpn VPNManager
|
|
statePath string
|
|
syncNftables func(globalCfg *config.State) // callback to resync firewall after config changes
|
|
}
|
|
|
|
func (h *vpnHandlers) Status(w http.ResponseWriter, r *http.Request) {
|
|
status, err := h.vpn.GetStatus()
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN status: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, status)
|
|
}
|
|
|
|
func (h *vpnHandlers) GetConfig(w http.ResponseWriter, r *http.Request) {
|
|
cfg, err := h.vpn.GetConfig()
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN config: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]any{
|
|
"enabled": cfg.Enabled,
|
|
"listenPort": cfg.ListenPort,
|
|
"address": cfg.Address,
|
|
"endpoint": cfg.Endpoint,
|
|
"dns": cfg.DNS,
|
|
"lanCIDR": cfg.LanCIDR,
|
|
"publicKey": h.vpn.GetPublicKey(),
|
|
})
|
|
}
|
|
|
|
func (h *vpnHandlers) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|
var req wireguard.Config
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
|
|
return
|
|
}
|
|
if req.ListenPort == 0 {
|
|
req.ListenPort = 51820
|
|
}
|
|
if err := h.vpn.SaveConfig(&req); err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save VPN config: %v", err))
|
|
return
|
|
}
|
|
// Resync nftables so the VPN listen port is automatically allowed/removed
|
|
if globalCfg, err := config.LoadState(h.statePath); err == nil {
|
|
go h.syncNftables(globalCfg)
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]string{"message": "VPN configuration saved"})
|
|
}
|
|
|
|
func (h *vpnHandlers) GenerateKeypair(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.vpn.GenerateKeypair(); err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate keypair: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]string{
|
|
"message": "Server keypair generated",
|
|
"publicKey": h.vpn.GetPublicKey(),
|
|
})
|
|
}
|
|
|
|
func (h *vpnHandlers) Apply(w http.ResponseWriter, r *http.Request) {
|
|
if err := h.vpn.Apply(); err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to apply VPN config: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]string{"message": "WireGuard interface applied successfully"})
|
|
}
|
|
|
|
func (h *vpnHandlers) ListPeers(w http.ResponseWriter, r *http.Request) {
|
|
peers, err := h.vpn.ListPeers()
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list peers: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]any{"peers": redactPeers(peers)})
|
|
}
|
|
|
|
func (h *vpnHandlers) AddPeer(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
|
|
respondError(w, http.StatusBadRequest, "name is required")
|
|
return
|
|
}
|
|
peer, err := h.vpn.AddPeer(req.Name)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add peer: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusCreated, redactPeer(peer))
|
|
}
|
|
|
|
func (h *vpnHandlers) GetPeerConfig(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
text, err := h.vpn.GeneratePeerConfigText(id)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate peer config: %v", err))
|
|
return
|
|
}
|
|
peer, err := h.vpn.GetPeer(id)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get peer: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]string{
|
|
"name": peer.Name,
|
|
"config": text,
|
|
})
|
|
}
|
|
|
|
func (h *vpnHandlers) DeletePeer(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
if err := h.vpn.DeletePeer(id); err != nil {
|
|
respondError(w, http.StatusNotFound, fmt.Sprintf("Failed to delete peer: %v", err))
|
|
return
|
|
}
|
|
respondJSON(w, http.StatusOK, map[string]string{"message": "Peer deleted"})
|
|
}
|
|
|
|
// redactPeers strips private keys from a peer list before sending to clients.
|
|
func redactPeers(peers []*wireguard.Peer) []map[string]any {
|
|
out := make([]map[string]any, len(peers))
|
|
for i, p := range peers {
|
|
out[i] = redactPeer(p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func redactPeer(p *wireguard.Peer) map[string]any {
|
|
return map[string]any{
|
|
"id": p.ID,
|
|
"name": p.Name,
|
|
"publicKey": p.PublicKey,
|
|
"allowedIPs": p.AllowedIPs,
|
|
"createdAt": p.CreatedAt,
|
|
}
|
|
}
|