fix: 校验端默认延迟不超过调度上限

This commit is contained in:
Nixevol
2026-09-30 16:22:48 +08:00
parent 43f847483b
commit b3787a0471
4 changed files with 93 additions and 7 deletions
+9
View File
@@ -917,6 +917,15 @@
- 备选方案:占满哈希池;未采用,给 MQTT 登录留槽位。 - 备选方案:占满哈希池;未采用,给 MQTT 登录留槽位。
- 影响:导入吞吐上升;冲突不再表现为无行号的 500。 - 影响:导入吞吐上升;冲突不再表现为无行号的 500。
### 复审修复 H-05
- 日期:2026-09-30
- 原条款:PRD F11 最远可定到 365 天后(可配置);审查 #48。
- 实际做法:`validateEndpointFields` 增加 `MaxScheduleSeconds`,开通/修改/导入与端侧改默认延迟同一上限(0 ≤ delay ≤ max)。界面 `:max` 在 W-05。
- 原因:后台原先只检查 `>= 0`,可设出永远发不出消息的端,极大值还会溢出。
- 备选方案:导入超限静默改成上限;未采用,改为行错误让管理员改表。
- 影响:超上限返回 400 或 CSV 行错误。
## 后台网页 W ## 后台网页 W
1. **W1–W3 阶段使用内存假数据,不请求真实 `/api/admin`** 1. **W1–W3 阶段使用内存假数据,不请求真实 `/api/admin`**
+22 -6
View File
@@ -233,7 +233,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
if req.DefaultDelaySeconds != nil { if req.DefaultDelaySeconds != nil {
delaySec = *req.DefaultDelaySeconds delaySec = *req.DefaultDelaySeconds
} }
if errMsg := validateEndpointFields(req.ID, req.Name, req.Remark, req.LoginPassword, req.TalkPassword, delaySec); errMsg != "" { if errMsg := validateEndpointFields(req.ID, req.Name, req.Remark, req.LoginPassword, req.TalkPassword, delaySec, h.maxScheduleSeconds()); errMsg != "" {
h.audit(actorString(p), "endpoint_create", req.ID, "bad_request", ip) h.audit(actorString(p), "endpoint_create", req.ID, "bad_request", ip)
httpx.WriteError(w, http.StatusBadRequest, "bad_request", errMsg) httpx.WriteError(w, http.StatusBadRequest, "bad_request", errMsg)
return return
@@ -341,10 +341,12 @@ func (h *Handler) handleEndpointPatch(w http.ResponseWriter, r *http.Request) {
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "备注过长") httpx.WriteError(w, http.StatusBadRequest, "bad_request", "备注过长")
return return
} }
if req.DefaultDelaySeconds != nil && *req.DefaultDelaySeconds < 0 { if req.DefaultDelaySeconds != nil {
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip) if msg := validateDelaySeconds(*req.DefaultDelaySeconds, h.maxScheduleSeconds()); msg != "" {
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "默认延迟无效") h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
return httpx.WriteError(w, http.StatusBadRequest, "bad_request", msg)
return
}
} }
hasMeta := req.Name != nil || req.Remark != nil || req.DefaultDelaySeconds != nil hasMeta := req.Name != nil || req.Remark != nil || req.DefaultDelaySeconds != nil
@@ -614,7 +616,7 @@ func (h *Handler) handleEndpointUnlock(w http.ResponseWriter, r *http.Request) {
httpx.WriteOK(w, map[string]any{}) httpx.WriteOK(w, map[string]any{})
} }
func validateEndpointFields(id, name, remark, loginPW, talkPW string, delaySec int64) string { func validateEndpointFields(id, name, remark, loginPW, talkPW string, delaySec, maxDelaySec int64) string {
if id != "" && !protocol.ValidEndpointID(id) { if id != "" && !protocol.ValidEndpointID(id) {
return "编号不合法" return "编号不合法"
} }
@@ -633,12 +635,26 @@ func validateEndpointFields(id, name, remark, loginPW, talkPW string, delaySec i
if !protocol.ValidTalkPassword(talkPW) { if !protocol.ValidTalkPassword(talkPW) {
return "对话密码不合法" return "对话密码不合法"
} }
if msg := validateDelaySeconds(delaySec, maxDelaySec); msg != "" {
return msg
}
return ""
}
func validateDelaySeconds(delaySec, maxDelaySec int64) string {
if delaySec < 0 { if delaySec < 0 {
return "默认延迟无效" return "默认延迟无效"
} }
if maxDelaySec > 0 && delaySec > maxDelaySec {
return "默认延迟无效"
}
return "" return ""
} }
func (h *Handler) maxScheduleSeconds() int64 {
return int64(h.cfg.Limits.MaxScheduleSeconds)
}
func writeCSVValidationError(w http.ResponseWriter, errs []csvLineError) { func writeCSVValidationError(w http.ResponseWriter, errs []csvLineError) {
httpx.WriteJSON(w, http.StatusBadRequest, httpx.Envelope{ httpx.WriteJSON(w, http.StatusBadRequest, httpx.Envelope{
OK: false, OK: false,
+1 -1
View File
@@ -244,7 +244,7 @@ func (h *Handler) validateImportCSV(ctx context.Context, raw []byte) ([]importPr
} }
delaySec = n delaySec = n
} }
if msg := validateEndpointFields(id, name, remark, loginPW, talkPW, delaySec); msg != "" { if msg := validateEndpointFields(id, name, remark, loginPW, talkPW, delaySec, h.maxScheduleSeconds()); msg != "" {
errs = append(errs, csvLineError{Line: line, Reason: msg}) errs = append(errs, csvLineError{Line: line, Reason: msg})
continue continue
} }
+61
View File
@@ -0,0 +1,61 @@
package admin_test
import (
"encoding/json"
"io"
"net/http"
"testing"
)
func TestCreateRejectsDefaultDelayOverMax(t *testing.T) {
_, srv, client, _, _ := setupEndpoints(t)
body := `{"id":"dly-c","name":"延迟","login_password":"password1","default_delay_seconds":31536001}`
res := postJSON(t, client, srv.URL+"/api/admin/endpoints", body, csrfHeaders())
env := decodeEnv(t, res)
if res.StatusCode != http.StatusBadRequest {
t.Fatalf("want 400 got %d %+v", res.StatusCode, env)
}
}
func TestPatchRejectsDefaultDelayOverMax(t *testing.T) {
_, srv, client, _, _ := setupEndpoints(t)
res := postJSON(t, client, srv.URL+"/api/admin/endpoints",
`{"id":"dly-p","name":"延迟","login_password":"password1"}`, csrfHeaders())
env := decodeEnv(t, res)
if res.StatusCode != 200 || !env.OK {
t.Fatalf("create: %d %+v", res.StatusCode, env)
}
res = doReq(t, client, http.MethodPatch, srv.URL+"/api/admin/endpoints/dly-p",
`{"default_delay_seconds":31536001}`, csrfHeaders())
env = decodeEnv(t, res)
if res.StatusCode != http.StatusBadRequest {
t.Fatalf("want 400 got %d %+v", res.StatusCode, env)
}
}
func TestImportRejectsDefaultDelayOverMax(t *testing.T) {
_, srv, client, _, _ := setupEndpoints(t)
csvBody := "" +
"id,name,login_password,talk_password,default_delay_seconds,remark\n" +
"dly-i,甲,password1,,31536001,\n"
res := postImportCSV(t, client, srv.URL, csvBody)
raw, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
if res.StatusCode != http.StatusBadRequest {
t.Fatalf("want 400 got %d body=%s", res.StatusCode, raw)
}
var parsed struct {
Data *struct {
Errors []struct {
Line int `json:"line"`
Reason string `json:"reason"`
} `json:"errors"`
} `json:"data"`
}
if err := json.Unmarshal(raw, &parsed); err != nil {
t.Fatal(err)
}
if parsed.Data == nil || len(parsed.Data.Errors) == 0 {
t.Fatalf("want row error, body=%s", raw)
}
}