package handlers import ( "encoding/json" "net/http" "strings" "nano-router/config" "nano-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"` NAT bool `json:"nat"` } appCfg, _ := config.Load() var natIfaces []string if appCfg != nil { natIfaces = appCfg.NAT.Interfaces } 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 := "" ifaceType := "" if appCfg != nil && appCfg.Interfaces != nil { if ic, ok := appCfg.Interfaces[name]; ok { label = ic.Label ifaceType = ic.Type } } s.Type = ifaceType if s.Type == "" && s.Gateway != "" { s.Type = "wan" } if s.Type == "" && s.Mode != "loopback" { s.Type = "lan" } isNAT := false for _, ni := range natIfaces { if ni == name { isNAT = true break } } result = append(result, iface{s, hasPending, label, isNAT}) } for name, cfg := range network.GetAllPending() { if existingNames[name] || !network.IsVLAN(name) { continue } s := &network.InterfaceStats{ Name: name, State: "unknown", Mode: cfg.Mode, Type: cfg.Type, IPv6: []string{}, } if cfg.Mode == "static" { s.IPv4 = cfg.Address s.IPv4Mask = cfg.Netmask s.Gateway = cfg.Gateway } if s.Type == "" && s.Gateway != "" { s.Type = "wan" } if s.Type == "" { s.Type = "lan" } label := cfg.Label if label == "" && appCfg != nil && appCfg.Interfaces != nil { if ic, ok := appCfg.Interfaces[name]; ok { label = ic.Label } } isNAT := false for _, ni := range natIfaces { if ni == name { isNAT = true break } } result = append(result, iface{s, true, label, isNAT}) } 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.LinkUp(name) case "down": err = network.LinkDown(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 := "" ifaceType := "" if appCfg != nil && appCfg.Interfaces != nil { if ic, ok2 := appCfg.Interfaces[name]; ok2 { label = ic.Label ifaceType = ic.Type } } if cfg := network.GetPendingConfig(name); cfg != nil { if cfg.Label != "" { label = cfg.Label } if cfg.Type != "" { ifaceType = cfg.Type } ok(w, map[string]interface{}{"config": cfg, "pending": true, "label": label, "type": ifaceType}) return } fileCfg, err := network.ParseConfig() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } if cfg, exists := fileCfg[name]; exists { if ifaceType == "" { ifaceType = cfg.Type } ok(w, map[string]interface{}{"config": cfg, "pending": false, "label": label, "type": ifaceType}) } else { defaultType := "lan" ok(w, map[string]interface{}{ "config": &network.InterfaceConfig{Name: name, Auto: true, Mode: "dhcp", Type: defaultType, Extra: map[string]string{}}, "pending": false, "label": label, "type": defaultType, }) } 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.Type != "wan" && cfg.Type != "lan" { fail(w, http.StatusBadRequest, "type must be 'wan' or 'lan'") return } if network.IsVLAN(name) && cfg.Type != "lan" { fail(w, http.StatusBadRequest, "VLAN interface must be type 'lan'") return } if cfg.Type == "lan" { if cfg.Mode == "dhcp" { fail(w, http.StatusBadRequest, "LAN interface cannot use DHCP mode") return } if cfg.Gateway != "" { fail(w, http.StatusBadRequest, "LAN interface cannot have a gateway") return } if len(cfg.DNS) > 0 { fail(w, http.StatusBadRequest, "LAN interface cannot have DNS servers") return } } if cfg.Type == "wan" && cfg.Mode == "static" && cfg.Address == "" { fail(w, http.StatusBadRequest, "WAN interface in static mode requires an IP address") return } if network.IsVLAN(name) { parent := network.VLANParent(name) appCfgCheck, _ := config.Load() if appCfgCheck != nil && appCfgCheck.Interfaces != nil { if pic, ok := appCfgCheck.Interfaces[parent]; ok && pic.Type == "wan" { fail(w, http.StatusBadRequest, "VLAN cannot be created on a WAN interface ("+parent+")") return } } } if msg, overlaps := checkInterfaceOverlap(&cfg); overlaps { fail(w, http.StatusConflict, msg) return } 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, Type: cfg.Type, 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") } }