Files
alpine-router/handlers/clients.go
2026-04-15 11:38:26 +03:00

228 lines
5.6 KiB
Go

package handlers
import (
"encoding/json"
"log"
"net/http"
"strings"
"nano-router/clients"
"nano-router/config"
"nano-router/dhcp"
)
func HandleClients(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
list, err := clients.GetAll()
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, list)
}
func HandleClientUpdate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
mac := strings.TrimPrefix(r.URL.Path, "/api/clients/update/")
if mac == "" {
fail(w, http.StatusBadRequest, "mac address required")
return
}
var req struct {
Hostname string `json:"hostname"`
Blocked bool `json:"blocked"`
StaticIP string `json:"static_ip"`
Policy string `json:"policy"` // "disabled" | "direct" | "vpn" | ""
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
return
}
if err := updateClient(mac, req.Hostname, req.Blocked, req.StaticIP, req.Policy); err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
go applyBlockedFirewall()
go applyDHCPStaticBindings()
ok(w, map[string]string{"message": "updated"})
}
// HandleClientPolicyDefault handles GET/POST for the default client routing policy.
func HandleClientPolicyDefault(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
cfg, err := config.Load()
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, map[string]string{"default": cfg.ClientPolicy.Default})
case http.MethodPost:
var req struct {
Default string `json:"default"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
return
}
if req.Default != "disabled" && req.Default != "direct" && req.Default != "vpn" {
fail(w, http.StatusBadRequest, "invalid policy: must be disabled, direct, or vpn")
return
}
cfg, err := config.Load()
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
cfg.ClientPolicy.Default = req.Default
if err := config.Save(cfg); err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
go applyBlockedFirewall()
ok(w, map[string]string{"default": req.Default})
default:
fail(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
// HandleClientPolicyApplyAll sets the given policy on every known device.
func HandleClientPolicyApplyAll(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
fail(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
var req struct {
Policy string `json:"policy"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "invalid json: "+err.Error())
return
}
if req.Policy != "disabled" && req.Policy != "direct" && req.Policy != "vpn" {
fail(w, http.StatusBadRequest, "invalid policy: must be disabled, direct, or vpn")
return
}
cfg, err := config.Load()
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
for i := range cfg.KnownDevices {
cfg.KnownDevices[i].Policy = req.Policy
// Keep Blocked flag consistent: disabled policy means blocked.
cfg.KnownDevices[i].Blocked = req.Policy == "disabled"
}
if err := config.Save(cfg); err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
go applyBlockedFirewall()
ok(w, map[string]int{"updated": len(cfg.KnownDevices)})
}
func updateClient(mac, hostname string, blocked bool, staticIP, policy string) error {
cfg, err := config.Load()
if err != nil {
return err
}
found := false
for i := range cfg.KnownDevices {
if cfg.KnownDevices[i].MAC == mac {
cfg.KnownDevices[i].Hostname = hostname
cfg.KnownDevices[i].StaticIP = staticIP
cfg.KnownDevices[i].Policy = policy
// Derive Blocked from policy for backward compatibility.
cfg.KnownDevices[i].Blocked = policy == "disabled"
found = true
break
}
}
if !found {
cfg.KnownDevices = append(cfg.KnownDevices, config.KnownDevice{
MAC: mac,
Hostname: hostname,
StaticIP: staticIP,
Policy: policy,
Blocked: policy == "disabled",
})
}
return config.Save(cfg)
}
func applyDHCPStaticBindings() {
if !dhcp.IsInstalled() {
return
}
cfg, err := config.Load()
if err != nil {
log.Printf("Warning: load config for DHCP static bindings: %v", err)
return
}
var bindings []dhcp.StaticBinding
for _, kd := range cfg.KnownDevices {
if kd.StaticIP != "" && kd.MAC != "" {
bindings = append(bindings, dhcp.StaticBinding{
MAC: kd.MAC,
Host: kd.Hostname,
IP: kd.StaticIP,
})
}
}
dhcpCfg := &dhcp.Config{
Enabled: cfg.DHCP.Enabled,
Pools: make([]dhcp.Pool, len(cfg.DHCP.Pools)),
}
for i, p := range cfg.DHCP.Pools {
dhcpCfg.Pools[i] = dhcp.Pool{
Interface: p.Interface,
Enabled: p.Enabled,
Subnet: p.Subnet,
Netmask: p.Netmask,
RangeStart: p.RangeStart,
RangeEnd: p.RangeEnd,
Router: p.Router,
DNS: p.DNS,
LeaseTime: p.LeaseTime,
}
}
if err := dhcp.WriteConfigsWithBindings(dhcpCfg, bindings); err != nil {
log.Printf("Warning: write dnsmasq config with static bindings: %v", err)
return
}
if dhcpCfg.Enabled {
if err := dhcp.ServiceRestart(); err != nil {
log.Printf("Warning: restart dnsmasq after static binding update: %v", err)
} else {
log.Printf("dnsmasq restarted with %d static bindings", len(bindings))
}
}
}