0.10.1: 安全与业务逻辑加固、新品牌与部署加固
三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
>4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cron implements the standard five-field cron format: minute, hour,
|
||||
// day-of-month, month, day-of-week. Lists, ranges and steps are supported.
|
||||
type Cron struct {
|
||||
minute, hour, day, month, weekday field
|
||||
dayWildcard, weekdayWildcard bool
|
||||
}
|
||||
|
||||
type field struct {
|
||||
min, max int
|
||||
values map[int]bool
|
||||
}
|
||||
|
||||
func ParseCron(expression string) (Cron, error) {
|
||||
parts := strings.Fields(expression)
|
||||
if len(parts) != 5 {
|
||||
return Cron{}, errors.New("cron 表达式必须包含 5 段: 分 时 日 月 周")
|
||||
}
|
||||
definitions := [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}}
|
||||
fields := make([]field, 5)
|
||||
for i, part := range parts {
|
||||
parsed, err := parseField(part, definitions[i][0], definitions[i][1], i == 4)
|
||||
if err != nil {
|
||||
return Cron{}, fmt.Errorf("cron 第 %d 段无效: %w", i+1, err)
|
||||
}
|
||||
fields[i] = parsed
|
||||
}
|
||||
return Cron{minute: fields[0], hour: fields[1], day: fields[2], month: fields[3], weekday: fields[4], dayWildcard: parts[2] == "*", weekdayWildcard: parts[4] == "*"}, nil
|
||||
}
|
||||
|
||||
func parseField(raw string, minimum, maximum int, weekday bool) (field, error) {
|
||||
result := field{min: minimum, max: maximum, values: map[int]bool{}}
|
||||
for _, item := range strings.Split(raw, ",") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
return field{}, errors.New("存在空列表项")
|
||||
}
|
||||
base, stepText, hasStep := strings.Cut(item, "/")
|
||||
step := 1
|
||||
if hasStep {
|
||||
var err error
|
||||
step, err = strconv.Atoi(stepText)
|
||||
if err != nil || step < 1 || step > maximum-minimum+1 {
|
||||
return field{}, errors.New("步长无效")
|
||||
}
|
||||
}
|
||||
start, end := minimum, maximum
|
||||
switch {
|
||||
case base == "*":
|
||||
case strings.Contains(base, "-"):
|
||||
left, right, _ := strings.Cut(base, "-")
|
||||
var err error
|
||||
start, err = cronNumber(left, minimum, maximum, weekday)
|
||||
if err != nil {
|
||||
return field{}, err
|
||||
}
|
||||
end, err = cronNumber(right, minimum, maximum, weekday)
|
||||
if err != nil || start > end {
|
||||
return field{}, errors.New("范围无效")
|
||||
}
|
||||
default:
|
||||
var err error
|
||||
start, err = cronNumber(base, minimum, maximum, weekday)
|
||||
if err != nil {
|
||||
return field{}, err
|
||||
}
|
||||
end = start
|
||||
if hasStep {
|
||||
end = maximum
|
||||
}
|
||||
}
|
||||
for value := start; value <= end; value += step {
|
||||
if weekday && value == 7 {
|
||||
value = 0
|
||||
result.values[value] = true
|
||||
break
|
||||
}
|
||||
result.values[value] = true
|
||||
}
|
||||
}
|
||||
if len(result.values) == 0 {
|
||||
return field{}, errors.New("没有可用取值")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func cronNumber(raw string, minimum, maximum int, weekday bool) (int, error) {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value < minimum || value > maximum {
|
||||
return 0, fmt.Errorf("%q 超出 %d-%d", raw, minimum, maximum)
|
||||
}
|
||||
if weekday && value == 7 {
|
||||
return 7, nil
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (c Cron) Matches(value time.Time) bool {
|
||||
dayMatch := c.day.values[value.Day()]
|
||||
weekdayMatch := c.weekday.values[int(value.Weekday())]
|
||||
calendarMatch := dayMatch && weekdayMatch
|
||||
// Vixie cron semantics: when both day fields are restricted, either may match.
|
||||
if !c.dayWildcard && !c.weekdayWildcard {
|
||||
calendarMatch = dayMatch || weekdayMatch
|
||||
}
|
||||
return c.minute.values[value.Minute()] && c.hour.values[value.Hour()] && c.month.values[int(value.Month())] && calendarMatch
|
||||
}
|
||||
|
||||
func (c Cron) Next(after time.Time, location *time.Location) (time.Time, error) {
|
||||
if location == nil {
|
||||
location = time.UTC
|
||||
}
|
||||
candidate := after.UTC().Truncate(time.Minute).Add(time.Minute)
|
||||
deadline := candidate.AddDate(5, 0, 0)
|
||||
for candidate.Before(deadline) {
|
||||
if c.Matches(candidate.In(location)) {
|
||||
return candidate, nil
|
||||
}
|
||||
candidate = candidate.Add(time.Minute)
|
||||
}
|
||||
return time.Time{}, errors.New("未来 5 年内无匹配执行时间")
|
||||
}
|
||||
Reference in New Issue
Block a user