Files
EveryPublish/client/core/internal/localserver/localserver.go
T

164 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}