package main import ( "bufio" "context" "encoding/json" "fmt" "io/fs" "net/http" "os" "os/exec" "strings" "hnyfkj.com.cn/rtu/linux/utils/jsonrpc2" ) const noValue = "--" type LoginReq struct { Username string `json:"username"` Password string `json:"password"` } type LoginResp struct { Success bool `json:"success"` } func loginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req LoginReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) return } ok := req.Username == "admin" && req.Password == "admin123456" if ok { sessionID := createSession() addSession(sessionID) http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: sessionID, Path: "/", HttpOnly: true, SameSite: http.SameSiteStrictMode, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(LoginResp{Success: ok}) } func logoutHandler(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie("session_id"); err == nil { sessionMu.Lock() delete(sessions, cookie.Value) sessionMu.Unlock() } http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: "", Path: "/", MaxAge: -1, }) http.Redirect(w, r, "/", http.StatusFound) } func getIMEIHandler(w http.ResponseWriter, r *http.Request) { data, err := os.ReadFile("/var/device_imei.txt") if err != nil { w.Write([]byte(noValue)) } else { w.Write(append([]byte("🆔"), data...)) } } func sshdStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } status := noValue cmd := exec.Command("systemctl", "is-active", "yfkj-sshd-mqtt-bridge.service") if out, err := cmd.Output(); err == nil { if strings.TrimSpace(string(out)) == "active" { status = "🟢 YFKJ-SSHD" } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": status}) } func (ui *WebUI) rootHandler(w http.ResponseWriter, r *http.Request) { path := r.URL.Path w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") // 1, 未登录时, 只能看登录页 if !checkSession(r) { data, err := fs.ReadFile(ui.fs, "login.html") if err != nil { http.NotFound(w, r) } else { w.Write(data) } return } // 2, 登录成功, 展示应用首页 if path == "/" { data, err := fs.ReadFile(ui.fs, "app.html") if err != nil { http.NotFound(w, r) } else { w.Write(data) } return } // 3, 在网站内跳转其它功能页 if strings.HasPrefix(path, "/pages/") { // path 形如 "/pages/page1.html" data, err := fs.ReadFile(ui.fs, path[1:]) if err != nil { http.NotFound(w, r) } else { w.Write(data) } return } // 4, 访问其它不认识的页面时 http.NotFound(w, r) } func serviceLogHandler(w http.ResponseWriter, r *http.Request) { unit := r.URL.Query().Get("unit") if unit == "" { http.Error(w, "missing unit", http.StatusBadRequest) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "not supported", http.StatusInternalServerError) return } cmd := exec.Command( "journalctl", "-f", "-u", unit, "-n", "10", "--no-pager", "-q", "-o", "short-iso", ) stdout, err := cmd.StdoutPipe() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := cmd.Start(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer func() { if cmd.Process != nil { cmd.Process.Kill() } cmd.Wait() }() scanner := bufio.NewScanner(stdout) for scanner.Scan() { select { case <-r.Context().Done(): return default: } if _, err := fmt.Fprintf(w, "data: %s\n\n", scanner.Text()); err != nil { return } flusher.Flush() } } func runShellAndWrite(w http.ResponseWriter, cmd string) { out, err := exec.Command("sh", "-c", cmd).CombinedOutput() if err != nil { http.Error(w, err.Error()+"\n"+string(out), http.StatusInternalServerError) return } w.Write(out) } func netInterfaces(w http.ResponseWriter, r *http.Request) { runShellAndWrite(w, "ifconfig") } func netRoutes(w http.ResponseWriter, r *http.Request) { runShellAndWrite(w, "route -n") } func netDNS(w http.ResponseWriter, r *http.Request) { runShellAndWrite(w, "cat /etc/resolv.conf") } func callRPCResult(ctx context.Context, port int, method string, params any, result any) error { client, err := jsonrpc2.NewRPCClient(fmt.Sprintf("http://127.0.0.1:%d/rpc", port)) if err != nil { return err } resp, err := client.Call(ctx, method, params) if err != nil { return err } return json.Unmarshal(resp.Result, result) } func callRPCResponse(w http.ResponseWriter, r *http.Request, port int, method string, params any) { client, err := jsonrpc2.NewRPCClient(fmt.Sprintf("http://127.0.0.1:%d/rpc", port)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } resp, err := client.Call(r.Context(), method, params) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(resp.Result) } func serviceLogLevel(w http.ResponseWriter, r *http.Request, port int) { switch r.Method { case http.MethodGet: callRPCResponse(w, r, port, "basic.getLogLevel", nil) case http.MethodPost: var req struct { Level string `json:"level"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } callRPCResponse(w, r, port, "basic.setLogLevel", map[string]string{"log_level": req.Level}) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func serviceLogLevelHandler(port int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { serviceLogLevel(w, r, port) } }