feat: 补齐第一批已知缺口(关闭握手 / 定时备份 / Origin 白名单 / DTO 收敛)

- httpapi:新增 POST /api/shutdown(admin 会话或 X-Shutdown-Token 常量时间比对)
  与 GET /api/rules/switches(recorder 可读,只回布尔开关);settings/standings 的
  重复 DTO 收敛到 internal/dto;standings.showIndividualMedals 贯通到设置读写与前端
- export:抽出 export.Service,新增自动备份定时任务(backup.intervalMinutes/keep),
  自动、手动、发布前三类备份共用同一把互斥锁与滚动清理
- realtime:/ws 去掉 InsecureSkipVerify,改用 OriginPatterns(同源与无 Origin 放行,
  跨站需命中 server.allowedOrigins)
- console:停止服务改走 POST /api/shutdown 优雅关闭(5 秒宽限后回退强杀),
  服务管理器新增「已连接的大屏实例」卡片
- deploy:build.bat 改用 for /f 读内部署名——set /p 在 chcp 65001 下返回空,
  导致最近这次构建的 exe 里根本没注入署名
- docs:备份策略、关闭超时、Origin 配置的说明与实现对齐
This commit is contained in:
AskaEth committed 2026-09-09 21:41:08 +08:00
1 parent 4d56c73ae6
commit 2c9ff65fc4
42 files changed
+1271 -275

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
package realtime
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/coder/websocket"
)
// dialOrigin 以指定 Origin 发起握手;origin 为空表示不带 Origin 头(非浏览器客户端)。
func dialOrigin(t *testing.T, srv *httptest.Server, origin string) error {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
opts := &websocket.DialOptions{}
if origin != "" {
opts.HTTPHeader = http.Header{"Origin": []string{origin}}
}
conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws", opts)
if err != nil {
return err
}
_ = conn.CloseNow()
return nil
}
func TestHandlerOrigin(t *testing.T) {
t.Run("同源放行", func(t *testing.T) {
e := newTestEnv(t)
// httptest.Server 的 URL 即页面 Origin:scheme + host 与请求 Host 同源。
if err := dialOrigin(t, e.srv, e.srv.URL); err != nil {
t.Fatalf("同源 Origin 应放行: %v", err)
}
})
t.Run("空 Origin 放行", func(t *testing.T) {
e := newTestEnv(t)
if err := dialOrigin(t, e.srv, ""); err != nil {
t.Fatalf("不带 Origin 的客户端应放行: %v", err)
}
})
t.Run("跨站拒绝", func(t *testing.T) {
e := newTestEnv(t)
if err := dialOrigin(t, e.srv, "http://evil.example"); err == nil {
t.Fatal("跨站 Origin 应拒绝")
}
})
t.Run("白名单放行", func(t *testing.T) {
e := newTestEnv(t)
e.hub.SetAllowedOrigins([]string{"http://evil.example"})
if err := dialOrigin(t, e.srv, "http://evil.example"); err != nil {
t.Fatalf("白名单内 Origin 应放行: %v", err)
}
if err := dialOrigin(t, e.srv, "http://other.example"); err == nil {
t.Fatal("白名单外的跨站 Origin 应拒绝")
}
})
}