// localserver 本机控制服务:仅 127.0.0.1,token 鉴权,供 WinUI 壳调用。 // 端点:/status /challenges /challenges/{id} /pair;端口与 token 写入 data/local.json。 package localserver import ( "crypto/rand" "crypto/subtle" "encoding/hex" "encoding/json" "log" "net" "net/http" "os" "path/filepath" "strings" "sync" "everypublish/shared/proto" ) // CoreState 核心状态接口(由 wsclient.Client 实现) type CoreState interface { Online() bool SessionID() string Challenges() []proto.Challenge SolveChallenge(challengeID, value string) error } // PairFunc 配对回调(main 提供:配对成功保存身份并启动连接) type PairFunc func(code, name string) error // Info 本地服务信息(写 local.json 供壳读取) type Info struct { Port int `json:"port"` Token string `json:"token"` } // ExtraFunc 额外状态字段(server/deviceId/name 等,闭包实时取值) type ExtraFunc func() map[string]interface{} // Server 本机控制服务 type Server struct { state CoreState pair PairFunc token string dir string extra ExtraFunc mu sync.Mutex addr string } // New 构造控制服务(extra 可空) func New(state CoreState, pair PairFunc, token, dir string, extra ExtraFunc) *Server { return &Server{state: state, pair: pair, token: token, dir: dir, extra: extra} } // Listen 绑定随机端口并服务,同时写 data/local.json func (s *Server) Listen() error { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return err } port := ln.Addr().(*net.TCPAddr).Port s.mu.Lock() s.addr = ln.Addr().String() s.mu.Unlock() raw, _ := json.Marshal(Info{Port: port, Token: s.token}) _ = os.WriteFile(filepath.Join(s.dir, "local.json"), raw, 0o600) log.Printf("[local] control server %s (local.json written)", s.addr) mux := http.NewServeMux() mux.HandleFunc("/status", s.handleStatus) mux.HandleFunc("/challenges", s.handleChallenges) mux.HandleFunc("/challenges/", s.handleSolve) mux.HandleFunc("/pair", s.handlePair) return http.Serve(ln, mux) } // Addr 实际监听地址(Listen 后可用) func (s *Server) Addr() string { s.mu.Lock() defer s.mu.Unlock() return s.addr } func (s *Server) auth(r *http.Request) bool { // 仅接受请求头;token 不进 URL(避免泄露进日志/历史)。常数时间比较防时序侧信道。 got := r.Header.Get("X-Agent-Token") if len(got) != len(s.token) { return false } return subtle.ConstantTimeCompare([]byte(got), []byte(s.token)) == 1 } func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { if !s.auth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } m := map[string]interface{}{ "online": s.state.Online(), "session": s.state.SessionID(), } if s.extra != nil { for k, v := range s.extra() { m[k] = v } } _ = json.NewEncoder(w).Encode(m) } func (s *Server) handleChallenges(w http.ResponseWriter, r *http.Request) { if !s.auth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } _ = json.NewEncoder(w).Encode(map[string]interface{}{"list": s.state.Challenges()}) } func (s *Server) handleSolve(w http.ResponseWriter, r *http.Request) { if !s.auth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } id := strings.TrimPrefix(r.URL.Path, "/challenges/") var req struct { Value string `json:"value"` } _ = json.NewDecoder(r.Body).Decode(&req) if err := s.state.SolveChallenge(id, req.Value); err != nil { _ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": err.Error()}) return } _ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 0}) } func (s *Server) handlePair(w http.ResponseWriter, r *http.Request) { if !s.auth(r) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } var req struct { Code string `json:"code"` Name string `json:"name"` } _ = json.NewDecoder(r.Body).Decode(&req) if req.Code == "" { _ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": "配对码必填"}) return } if err := s.pair(req.Code, req.Name); err != nil { _ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 1, "message": err.Error()}) return } _ = json.NewEncoder(w).Encode(map[string]interface{}{"code": 0}) } // RandToken 生成随机 token func RandToken() string { b := make([]byte, 16) _, _ = rand.Read(b) return hex.EncodeToString(b) }