加强初始化令牌并发保护
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-08-30 20:53:27 +08:00
parent 0d6f0060b9
commit 961d9ff222
3 changed files with 63 additions and 1 deletions

View File

@ -33,6 +33,7 @@ type setupAttempt struct {
type setupSecurity struct {
mu sync.Mutex
requestMu sync.Mutex
token string
tokenHash [32]byte
consumed bool
@ -143,6 +144,13 @@ func (s *setupSecurity) authorize(next http.HandlerFunc) http.HandlerFunc {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// 一次性令牌必须串行消费请求,避免两个并发请求在状态切换前同时进入初始化处理器。
s.requestMu.Lock()
defer s.requestMu.Unlock()
if !s.validToken(token) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
s.clearFailures(ip)
next.ServeHTTP(w, r)
if config.IsSetupReady() {

View File

@ -4,6 +4,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"sync"
"testing"
"git.pcux.cn/xuans/Smail/config"
@ -86,6 +87,46 @@ func TestSetupSecurityAcceptsTokenOnce(t *testing.T) {
}
}
func TestSetupSecuritySerializesConcurrentTokenRequests(t *testing.T) {
token := "0123456789abcdefghijklmnop"
security := withSetupSecurity(t, token)
started := make(chan struct{})
continueRequest := make(chan struct{})
var calls int
var callsMu sync.Mutex
handler := security.authorize(func(w http.ResponseWriter, r *http.Request) {
callsMu.Lock()
calls++
callsMu.Unlock()
close(started)
<-continueRequest
config.IsInit = true
w.WriteHeader(http.StatusNoContent)
})
results := make(chan int, 2)
for i := 0; i < 2; i++ {
go func() {
request := setupRequest(http.MethodPost)
request.Header.Set("X-Setup-Token", token)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
results <- recorder.Code
}()
}
<-started
close(continueRequest)
first, second := <-results, <-results
if (first != http.StatusNoContent && second != http.StatusNoContent) || (first == http.StatusNoContent && second == http.StatusNoContent) {
t.Fatalf("concurrent setup results = %d, %d; want exactly one success", first, second)
}
callsMu.Lock()
defer callsMu.Unlock()
if calls != 1 {
t.Fatalf("setup handler calls = %d, want 1", calls)
}
}
func TestSetupSecurityBlocksUnauthorizedSource(t *testing.T) {
token := "0123456789abcdefghijklmnop"
security := withSetupSecurity(t, token)

View File

@ -4,14 +4,23 @@ import (
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/signal"
"git.pcux.cn/xuans/Smail/utils/errors"
"sync"
)
var finishMu sync.Mutex
// Finish 标记初始化完成
func Finish() error {
finishMu.Lock()
defer finishMu.Unlock()
cfg, err := config.ReadConfig()
if err != nil {
return errors.Wrap(err)
}
if cfg.IsSetupReady() {
config.IsInit = true
return nil
}
cfg.SetupState = config.SetupStateReady
cfg.IsInit = true
@ -21,6 +30,10 @@ func Finish() error {
}
config.IsInit = true
// 初始化完成
signal.InitChan <- true
select {
case signal.InitChan <- true:
default:
// 初始化通知只需唤醒一次启动流程,避免重复完成请求阻塞响应。
}
return nil
}