server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警

This commit is contained in:
Qiufeng
2026-08-20 20:38:21 +08:00
parent bf25d8beac
commit cc8845dcec
44 changed files with 4995 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
// 假执行器:模拟客户端核心(配对 → WSS → 心跳 → 任务执行 → 挑战处理)。
// 用途:D4 本地闭环验收 + 联调诊断;D8 起由 client/core 真 Agent 取代。
package main
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"io"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"github.com/coder/websocket"
"everypublish/shared/proto"
)
var (
server = flag.String("server", "http://127.0.0.1:8090", "服务器地址")
user = flag.String("user", "demo@everypublish.io", "登录邮箱")
password = flag.String("pass", "demo-pass-2026", "登录密码")
devName = flag.String("name", "fake-agent-01", "设备名")
execMS = flag.Int("exec", 50, "模拟执行时长 ms")
keyPath = flag.String("key", "./data/fake-agent.ed25519", "设备私钥文件")
)
func main() {
flag.Parse()
log.SetFlags(log.Ltime | log.Lmicroseconds)
access := login(*server, *user, *password)
code := pairCode(*server, access)
pub, priv := loadOrCreateKey(*keyPath)
deviceID := pair(*server, code, pub)
log.Printf("paired device %d", deviceID)
wsURL := "ws" + (*server)[4:] + "/ws/agent"
conn, _, err := websocket.Dial(context.Background(), wsURL, nil)
if err != nil {
log.Fatalf("dial: %v", err)
}
defer conn.Close(websocket.StatusNormalClosure, "bye")
nonce := randHex(16)
ts := time.Now().UnixMilli()
sig := ed25519.Sign(priv, []byte(nonce+"|"+strconv.FormatInt(ts, 10)))
hello := proto.NewEnvelope(randHex(8), proto.TypeHello, proto.DeviceHello{
DeviceID: strconv.FormatUint(deviceID, 10),
Nonce: nonce,
TS: ts,
Sig: base64.StdEncoding.EncodeToString(sig),
Version: "0.1.0-fake",
})
if err = send(conn, hello); err != nil {
log.Fatalf("hello: %v", err)
}
_, raw, err := conn.Read(context.Background())
if err != nil {
log.Fatalf("hello.ack read: %v", err)
}
var ackEnv proto.Envelope
_ = json.Unmarshal(raw, &ackEnv)
if ackEnv.Type != proto.TypeHelloAck {
log.Fatalf("expect hello.ack, got %s", ackEnv.Type)
}
var ack proto.HelloAck
_ = json.Unmarshal(ackEnv.Payload, &ack)
log.Printf("online (session %s)", ack.SessionID)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go heartbeatLoop(ctx, conn, deviceID)
for {
_, raw, err = conn.Read(ctx)
if err != nil {
log.Fatalf("read: %v", err)
}
var env proto.Envelope
if err = json.Unmarshal(raw, &env); err != nil {
continue
}
switch env.Type {
case proto.TypeTaskPush:
var push proto.TaskPush
_ = json.Unmarshal(env.Payload, &push)
log.Printf("TASK push %s platform=%s title=%s", push.TaskID, push.Platform, push.Title)
_ = send(conn, proto.NewEnvelope(env.ID, proto.TypeTaskAck, proto.TaskAck{TaskID: push.TaskID, Accept: true}))
time.Sleep(time.Duration(*execMS) * time.Millisecond)
result := proto.TaskResult{
TaskID: push.TaskID,
Status: "success",
PublishedURL: "https://example.com/fake/" + push.TaskID,
Receipts: []string{},
FinishedAt: time.Now().UnixMilli(),
}
_ = send(conn, proto.NewEnvelope(randHex(8), proto.TypeTaskResult, result))
log.Printf("TASK %s done (%dms)", push.TaskID, *execMS)
case proto.TypeChallenge:
var ch proto.Challenge
_ = json.Unmarshal(env.Payload, &ch)
log.Printf("CHALLENGE %s kind=%s prompt=%s", ch.ChallengeID, ch.Kind, ch.Prompt)
_ = send(conn, proto.NewEnvelope(env.ID, proto.TypeChallengeAck, proto.ChallengeAck{ChallengeID: ch.ChallengeID, Action: "accept"}))
time.Sleep(time.Duration(*execMS) * time.Millisecond)
_ = send(conn, proto.NewEnvelope(randHex(8), proto.TypeChallengeSolve, proto.ChallengeSolve{ChallengeID: ch.ChallengeID, Value: "fake-qr-token"}))
log.Printf("CHALLENGE %s solved", ch.ChallengeID)
case proto.TypeHeartbeatAck:
default:
log.Printf("ignore msg type %s", env.Type)
}
}
}
func heartbeatLoop(ctx context.Context, conn *websocket.Conn, deviceID uint64) {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
_ = send(conn, proto.NewEnvelope(randHex(8), proto.TypeHeartbeat, proto.Heartbeat{DeviceID: strconv.FormatUint(deviceID, 10), TS: time.Now().UnixMilli()}))
}
}
}
func send(conn *websocket.Conn, msg *proto.Envelope) error {
raw, err := json.Marshal(msg)
if err != nil {
return err
}
wctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return conn.Write(wctx, websocket.MessageText, raw)
}
// ---- REST 辅助 ----
func httpJSON(method, url, token string, body interface{}) (int, []byte) {
var buf bytes.Buffer
if body != nil {
raw, _ := json.Marshal(body)
buf.Write(raw)
}
req, _ := http.NewRequest(method, url, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("http %s %s: %v", method, url, err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
return resp.StatusCode, raw
}
func field(data []byte, key string) string {
var m map[string]interface{}
_ = json.Unmarshal(data, &m)
if d, ok := m["data"].(map[string]interface{}); ok {
if v, ok := d[key]; ok {
switch t := v.(type) {
case string:
return t
case float64:
return strconv.FormatInt(int64(t), 10)
}
}
}
return ""
}
func login(base, email, password string) string {
code, raw := httpJSON("POST", base+"/api/v1/auth/login", "", ginH{"email": email, "password": password})
if code != 200 {
log.Fatalf("login failed: %s", string(raw))
}
log.Println("login ok")
return field(raw, "accessToken")
}
func pairCode(base, token string) string {
code, raw := httpJSON("POST", base+"/api/v1/agent/pair-code", token, nil)
if code != 200 {
log.Fatalf("pair-code failed: %s", string(raw))
}
return field(raw, "code")
}
func pair(base, code, pub string) uint64 {
httpCode, raw := httpJSON("POST", base+"/api/v1/agent/pair", "", ginH{"code": code, "deviceName": *devName, "os": "fake", "version": "0.1.0", "publicKey": pub})
if httpCode != 200 {
log.Fatalf("pair failed: %s", string(raw))
}
id, _ := strconv.ParseUint(field(raw, "deviceId"), 10, 64)
return id
}
type ginH map[string]interface{}
func loadOrCreateKey(path string) (string, ed25519.PrivateKey) {
if raw, err := os.ReadFile(path); err == nil {
seed, err := hex.DecodeString(string(raw))
if err == nil && len(seed) == ed25519.SeedSize {
priv := ed25519.NewKeyFromSeed(seed)
return base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)), priv
}
}
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("keygen: %v", err)
}
_ = os.MkdirAll(filepath.Dir(path), 0o755)
_ = os.WriteFile(path, []byte(hex.EncodeToString(priv.Seed())), 0o600)
return base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)), priv
}
func randHex(n int) string {
b := make([]byte, n)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}