Files
superidou 9501751792 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 币种维度)
2026-08-13 10:50:51 +08:00

132 lines
3.7 KiB
Go

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 年内无匹配执行时间")
}