client: Windows 客户端(WinUI3 壳 + agent-core;调试用 Electron 壳存档);localserver token 改为常数时间比较
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user