package handlers import ( "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "nano-router/config" "nano-router/mihomo" ) func HandleMihomoStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } ok(w, mihomo.Status()) } func HandleMihomoStart(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } if err := mihomo.Start(); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } saveMihomoEnabled(true) ok(w, map[string]string{"message": "mihomo started"}) } func HandleMihomoStop(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } if err := mihomo.Stop(); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } saveMihomoEnabled(false) ok(w, map[string]string{"message": "mihomo stopped"}) } func HandleMihomoRestart(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } if err := mihomo.Restart(); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } // Restart keeps enabled=true (already set when it was first started). ok(w, map[string]string{"message": "mihomo restarted"}) } // saveMihomoEnabled persists mihomo.enabled to config.yaml so the binary // auto-starts Mihomo on the next launch when enabled=true. func saveMihomoEnabled(enabled bool) { cfg, err := config.Load() if err != nil { log.Printf("Warning: load config to save mihomo enabled: %v", err) return } cfg.Mihomo.Enabled = enabled if err := config.Save(cfg); err != nil { log.Printf("Warning: save mihomo enabled state: %v", err) } } func HandleMihomoConfig(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: cfg, err := mihomo.LoadConfig() if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, cfg) case http.MethodPut: var cfg map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { fail(w, http.StatusBadRequest, "invalid json: "+err.Error()) return } if err := mihomo.SaveConfig(cfg); err != nil { fail(w, http.StatusInternalServerError, "save config: "+err.Error()) return } ok(w, map[string]string{"message": "config saved"}) default: fail(w, http.StatusMethodNotAllowed, "method not allowed") } } func HandleMihomoLogs(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } ok(w, mihomo.Logs()) } func HandleMihomoConfigYAML(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: data, err := os.ReadFile(mihomo.ConfigPath()) if err != nil { if os.IsNotExist(err) { w.WriteHeader(http.StatusNotFound) return } fail(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "text/yaml; charset=utf-8") w.Write(data) case http.MethodPut: data, err := io.ReadAll(r.Body) if err != nil { fail(w, http.StatusBadRequest, "read body: "+err.Error()) return } if err := os.MkdirAll(mihomo.DataDir(), 0755); err != nil { fail(w, http.StatusInternalServerError, "mkdir: "+err.Error()) return } tmp := mihomo.ConfigPath() + ".tmp" if err := os.WriteFile(tmp, data, 0644); err != nil { fail(w, http.StatusInternalServerError, "write: "+err.Error()) return } if err := os.Rename(tmp, mihomo.ConfigPath()); err != nil { fail(w, http.StatusInternalServerError, "rename: "+err.Error()) return } ok(w, map[string]string{"message": "config.yaml updated"}) default: fail(w, http.StatusMethodNotAllowed, "method not allowed") } } func HandleMihomoUploadCore(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { fail(w, http.StatusMethodNotAllowed, "method not allowed") return } if err := r.ParseMultipartForm(64 << 20); err != nil { fail(w, http.StatusBadRequest, "parse form: "+err.Error()) return } file, header, err := r.FormFile("core") if err != nil { fail(w, http.StatusBadRequest, "file required: "+err.Error()) return } defer file.Close() name := header.Filename for _, arch := range []string{"amd64", "arm64", "armv7"} { if strings.Contains(name, arch) { dstPath := filepath.Join(mihomo.CoresDir(), fmt.Sprintf("mihomo-linux-%s", arch)) if err := os.MkdirAll(mihomo.CoresDir(), 0755); err != nil { fail(w, http.StatusInternalServerError, "mkdir: "+err.Error()) return } dst, err := os.Create(dstPath) if err != nil { fail(w, http.StatusInternalServerError, "create: "+err.Error()) return } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { fail(w, http.StatusInternalServerError, "write: "+err.Error()) return } if err := os.Chmod(dstPath, 0755); err != nil { fail(w, http.StatusInternalServerError, "chmod: "+err.Error()) return } ok(w, map[string]string{"message": "core uploaded", "arch": arch}) return } } dstPath := filepath.Join(mihomo.CoresDir(), "mihomo-linux-amd64") if err := os.MkdirAll(mihomo.CoresDir(), 0755); err != nil { fail(w, http.StatusInternalServerError, "mkdir: "+err.Error()) return } dst, err := os.Create(dstPath) if err != nil { fail(w, http.StatusInternalServerError, "create: "+err.Error()) return } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { fail(w, http.StatusInternalServerError, "write: "+err.Error()) return } if err := os.Chmod(dstPath, 0755); err != nil { fail(w, http.StatusInternalServerError, "chmod: "+err.Error()) return } ok(w, map[string]string{"message": "core uploaded", "path": dstPath}) }