package handlers import ( "encoding/json" "net/http" "strings" "alpine-router/config" "alpine-router/network" ) type apiResp struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func ok(w http.ResponseWriter, data interface{}) { writeJSON(w, http.StatusOK, apiResp{Success: true, Data: data}) } func fail(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, apiResp{Error: msg}) } func HandleInterfaces(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } names, err := network.GetInterfaces() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } fileCfg, _ := network.ParseConfig() type iface struct { *network.InterfaceStats Pending bool `json:"pending"` Label string `json:"label,omitempty"` } appCfg, _ := config.Load() result := make([]iface, 0, len(names)) existingNames := map[string]bool{} for _, name := range names { existingNames[name] = true s, err := network.GetInterfaceStats(name) if err != nil { continue } if cfg, ok := fileCfg[name]; ok { s.Mode = cfg.Mode } hasPending := network.GetPendingConfig(name) != nil label := "" if appCfg != nil && appCfg.Interfaces != nil { if ic, ok := appCfg.Interfaces[name]; ok { label = ic.Label } } result = append(result, iface{s, hasPending, label}) } // Also include pending VLAN configs not yet present in the system. for name, cfg := range network.GetAllPending() { if existingNames[name] || !network.IsVLAN(name) { continue } s := &network.InterfaceStats{ Name: name, State: "unknown", Mode: cfg.Mode, IPv6: []string{}, } if cfg.Mode == "static" { s.IPv4 = cfg.Address s.IPv4Mask = cfg.Netmask s.Gateway = cfg.Gateway } label := cfg.Label if label == "" && appCfg != nil && appCfg.Interfaces != nil { if ic, ok := appCfg.Interfaces[name]; ok { label = ic.Label } } result = append(result, iface{s, true, label}) } ok(w, result) } func HandleInterfaceSingle(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } name := strings.TrimPrefix(r.URL.Path, "/api/interfaces/") s, err := network.GetInterfaceStats(name) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, s) } func HandleInterfaceAction(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } suffix := strings.TrimPrefix(r.URL.Path, "/api/interfaces/") parts := strings.SplitN(suffix, "/", 2) if len(parts) != 2 { fail(w, http.StatusBadRequest, "invalid path") return } name, action := parts[0], parts[1] var err error switch action { case "up": err = network.IfUp(name) case "down": err = network.IfDown(name) case "restart": err = network.IfRestart(name) case "delete": if !network.IsVLAN(name) { fail(w, http.StatusBadRequest, "delete is only supported for VLAN interfaces") return } network.ClearPendingConfig(name) err = network.DeleteVLAN(name) default: fail(w, http.StatusBadRequest, "unknown action: "+action) return } if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]string{"message": action + " ok"}) } func HandleConfig(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/api/config/") if name == "" { fail(w, http.StatusBadRequest, "interface name required") return } switch r.Method { case http.MethodGet: appCfg, _ := config.Load() label := "" if appCfg != nil && appCfg.Interfaces != nil { if ic, ok2 := appCfg.Interfaces[name]; ok2 { label = ic.Label } } if cfg := network.GetPendingConfig(name); cfg != nil { if cfg.Label != "" { label = cfg.Label } ok(w, map[string]interface{}{"config": cfg, "pending": true, "label": label}) return } fileCfg, err := network.ParseConfig() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } if cfg, exists := fileCfg[name]; exists { ok(w, map[string]interface{}{"config": cfg, "pending": false, "label": label}) } else { ok(w, map[string]interface{}{ "config": &network.InterfaceConfig{Name: name, Auto: true, Mode: "dhcp", Extra: map[string]string{}}, "pending": false, "label": label, }) } case http.MethodPost: var cfg network.InterfaceConfig if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { fail(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } cfg.Name = name if cfg.Extra == nil { cfg.Extra = map[string]string{} } network.SetPendingConfig(&cfg) appCfg, err := config.Load() if err != nil { fail(w, http.StatusInternalServerError, "load config: "+err.Error()) return } if appCfg.Interfaces == nil { appCfg.Interfaces = map[string]*config.InterfaceConfig{} } appCfg.Interfaces[name] = &config.InterfaceConfig{ Label: cfg.Label, Auto: cfg.Auto, Mode: cfg.Mode, Address: cfg.Address, Netmask: cfg.Netmask, Gateway: cfg.Gateway, DNS: cfg.DNS, Extra: cfg.Extra, } if err := config.Save(appCfg); err != nil { fail(w, http.StatusInternalServerError, "save config.yaml: "+err.Error()) return } ok(w, map[string]string{"message": "saved as pending"}) case http.MethodDelete: network.ClearPendingConfig(name) ok(w, map[string]string{"message": "pending cleared"}) default: fail(w, http.StatusMethodNotAllowed, "method not allowed") } } func HandlePending(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } p := network.GetAllPending() names := make([]string, 0, len(p)) for n := range p { names = append(names, n) } ok(w, names) } func HandleApply(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } errs := network.ApplyPending() if len(errs) > 0 { msgs := map[string]string{} for k, e := range errs { msgs[k] = e.Error() } writeJSON(w, http.StatusInternalServerError, apiResp{Error: "partial failure", Data: msgs}) return } ok(w, map[string]string{"message": "applied"}) } func HandleConfigYAML(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: data, err := config.Load() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, data) case http.MethodPut: var newCfg config.AppConfig if err := json.NewDecoder(r.Body).Decode(&newCfg); err != nil { fail(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } config.EnsureDefaults(&newCfg) if err := config.Save(&newCfg); err != nil { fail(w, http.StatusInternalServerError, "save: "+err.Error()) return } ok(w, map[string]string{"message": "config updated"}) default: fail(w, http.StatusMethodNotAllowed, "method not allowed") } }