server: Go 服务器(REST+WSS 网关+JWT+Argon2+配对+素材直链+任务状态机);修复并发下线 send-on-closed-channel、下发查询 SQL 优先级、配对码原子占用、上传体积上限、JWT 默认密钥告警
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user