From 57944398e6951021882f57f01abf225731e0ebe9 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 10 Jun 2026 13:42:15 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A8=E9=87=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yml | 28 +++++---- Makefile | 2 +- internal/apps/admin/db_manage/routers.go | 12 +++- internal/apps/admin/status/routers.go | 11 ++-- internal/apps/admin/system_config/routers.go | 2 +- internal/apps/admin/user/errs.go | 2 +- internal/apps/cap/errs.go | 4 +- internal/apps/oauth/errs.go | 2 +- internal/apps/oauth/sources.go | 6 +- internal/apps/risk_control/middleware.go | 10 +++- internal/apps/upload/routers.go | 2 +- internal/apps/user/errs.go | 24 ++++---- internal/apps/user/logics.go | 22 ++++--- internal/apps/user/routers.go | 4 +- internal/apps/user/tasks.go | 2 +- internal/config/config.go | 2 +- internal/db/idgen/snowflake.go | 6 +- internal/model/auth_source.go | 8 +-- internal/model/errs.go | 2 +- internal/model/system_configs.go | 4 +- internal/router/router.go | 63 +++++++++++--------- internal/storage/cache.go | 4 +- internal/util/cap/manager.go | 4 +- internal/util/http_clients.go | 2 +- internal/util/mail/errs.go | 2 +- internal/util/mail/mail.go | 33 ++++++---- internal/util/mail/mail_test.go | 3 +- 27 files changed, 155 insertions(+), 111 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d13785c3..8b0cf748 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,36 +17,38 @@ linters: # 代码坏味道 - dupl # 重复代码 - mnd # 魔法数字 - - gocyclo # 圈复杂度 - cyclop # 包/函数复杂度 - nestif # if 嵌套太深 - maintidx # 维护性指数 - revive # 风格/命名/坏味道 - gocritic # 各类代码问题 + - funlen # 函数过长 + + - gosec # 安全问题检查 + - bodyclose # HTTP response body 没有正确关闭 + - noctx # 没有传递 context.Context + - contextcheck # 其他检查 + - sqlclosecheck # SQL rows 没有正确关闭 + - unconvert # 不必要的类型转换 + - nilerr # 函数返回 nil 错误 settings: dupl: - threshold: 150 - - gocyclo: - min-complexity: 20 + threshold: 120 cyclop: - max-complexity: 15 + max-complexity: 20 package-average: 10 - funlen: - lines: 150 - statements: 80 - nestif: min-complexity: 5 + funlen: + lines: 200 + statements: 100 + mnd: checks: - argument - - case - condition - - operation - return - - assign diff --git a/Makefile b/Makefile index 2eb93b34..f45b8297 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,8 @@ build-embedded: go build -tags embed_frontend -o bin/wavelet main.go code-check: - cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 golangci-lint run + cd frontend && pnpm tsc --noEmit --jsx preserve && npx eslint . --max-warnings 0 build-test: @echo "==> Running frontend and backend build tests in parallel..." diff --git a/internal/apps/admin/db_manage/routers.go b/internal/apps/admin/db_manage/routers.go index 01f24a9f..1399a2da 100644 --- a/internal/apps/admin/db_manage/routers.go +++ b/internal/apps/admin/db_manage/routers.go @@ -118,7 +118,11 @@ func getSQLiteOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { var sizeStr string if fi, err := os.Stat(name); err == nil { - sizeStr = formatBytes(uint64(fi.Size())) + size := fi.Size() + if size < 0 { + size = 0 + } + sizeStr = formatBytes(uint64(size)) } else { sizeStr = "0 B" } @@ -160,7 +164,11 @@ func getPostgresOverview(gormDB *gorm.DB) (DBOverviewResponse, error) { var sizeStr string var sizeBytes sql.NullInt64 if err := gormDB.Raw("SELECT pg_database_size(current_database())").Scan(&sizeBytes).Error; err == nil && sizeBytes.Valid { - sizeStr = formatBytes(uint64(sizeBytes.Int64)) + size := sizeBytes.Int64 + if size < 0 { + size = 0 + } + sizeStr = formatBytes(uint64(size)) } else { sizeStr = "0 B" } diff --git a/internal/apps/admin/status/routers.go b/internal/apps/admin/status/routers.go index 617912f2..bf091c6c 100644 --- a/internal/apps/admin/status/routers.go +++ b/internal/apps/admin/status/routers.go @@ -147,9 +147,12 @@ func GetSystemStatus(c *gin.Context) { numGoroutine := runtime.NumGoroutine() var lastGCTime string - if m.LastGC > 0 { + switch { + case m.LastGC > 0 && m.LastGC <= math.MaxInt64: lastGCTime = formatDuration(time.Since(time.Unix(0, int64(m.LastGC)))) - } else { + case m.LastGC > 0: + lastGCTime = "未知" + default: lastGCTime = "无" } @@ -286,7 +289,7 @@ func exportSQLite(c *gin.Context) { path = "./data/wavelet.db" } - f, err := os.Open(path) + f, err := os.Open(path) //nolint:gosec // path is loaded from server startup configuration, not user input if err != nil { c.JSON(http.StatusInternalServerError, util.Err("无法打开数据库文件: "+err.Error())) return @@ -329,7 +332,7 @@ func exportPostgres(c *gin.Context) { dbCfg.Database, } - cmd := exec.CommandContext(c.Request.Context(), pgDumpPath, args...) + cmd := exec.CommandContext(c.Request.Context(), pgDumpPath, args...) //nolint:gosec // pgDumpPath is a looked up command path, args are from database configuration if dbCfg.Password != "" { cmd.Env = append(os.Environ(), "PGPASSWORD="+dbCfg.Password) } else { diff --git a/internal/apps/admin/system_config/routers.go b/internal/apps/admin/system_config/routers.go index f66bf531..275ed5c6 100644 --- a/internal/apps/admin/system_config/routers.go +++ b/internal/apps/admin/system_config/routers.go @@ -275,7 +275,7 @@ func TestSMTP(c *gin.Context) {

If you received this message, your SMTP configuration is correct and mail sending is working properly.

Sent from Wavelet.

` - logs, err := mail.SendMailWithLog(cfg, req.To, subject, body) + logs, err := mail.SendMailWithLog(c.Request.Context(), cfg, req.To, subject, body) resp := TestSMTPResponse{ Success: err == nil, Log: logs, diff --git a/internal/apps/admin/user/errs.go b/internal/apps/admin/user/errs.go index fc1dffb7..e6809c8b 100644 --- a/internal/apps/admin/user/errs.go +++ b/internal/apps/admin/user/errs.go @@ -14,6 +14,6 @@ const ( deleteUserFailed = "删除用户失败" usernameExists = "用户名已存在" usernameRequired = "用户名不能为空" - passwordTooShort = "密码长度不能少于 8 位" + passwordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials createUserFailed = "创建用户失败" ) diff --git a/internal/apps/cap/errs.go b/internal/apps/cap/errs.go index 98dcf0f5..9fd43234 100644 --- a/internal/apps/cap/errs.go +++ b/internal/apps/cap/errs.go @@ -5,6 +5,6 @@ package cap const ( - errCapTokenMissing = "验证码验证失败,缺少验证码凭证" - errCapTokenInvalidOrExpired = "验证码校验失败或已过期,请重试" + errCapTokenMissing = "验证码验证失败,缺少验证码凭证" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errCapTokenInvalidOrExpired = "验证码校验失败或已过期,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials ) diff --git a/internal/apps/oauth/errs.go b/internal/apps/oauth/errs.go index 31493ef9..09da5a2c 100644 --- a/internal/apps/oauth/errs.go +++ b/internal/apps/oauth/errs.go @@ -7,7 +7,7 @@ package oauth // OAuth 认证相关错误消息 const ( InvalidState = "非法登录请求" - IDTokenVerifyFailed = "ID Token 验证失败" + IDTokenVerifyFailed = "ID Token 验证失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials IDTokenVerifyFailedFormat = "%s: %w" NonceMismatch = "nonce 不匹配,可能存在重放攻击" NoActiveAuthSource = "未配置可用认证源" diff --git a/internal/apps/oauth/sources.go b/internal/apps/oauth/sources.go index 69042013..d395514e 100644 --- a/internal/apps/oauth/sources.go +++ b/internal/apps/oauth/sources.go @@ -469,7 +469,7 @@ func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthS c.JSON(http.StatusInternalServerError, util.Err(err.Error())) return } - if err := model.BindExternalAccount(&model.ExternalAccount{ + if err := model.BindExternalAccount(ctx, &model.ExternalAccount{ AuthSourceID: source.ID, UserID: user.ID, ExternalID: userInfo.Sub, @@ -488,7 +488,7 @@ func handleCallbackBind(ctx context.Context, c *gin.Context, source *model.AuthS func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.AuthSource, userInfo *model.OAuthUserInfo) { var user model.User - account, err := model.FindExternalAccount(source.ID, userInfo.Sub) + account, err := model.FindExternalAccount(ctx, source.ID, userInfo.Sub) switch { case err == nil: if err := db.DB(ctx).First(&user, "id = ?", account.UserID).Error; err != nil { @@ -549,7 +549,7 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *model.A c.JSON(http.StatusInternalServerError, util.Err(err.Error())) return model.User{}, false } - if err := model.BindExternalAccount(&model.ExternalAccount{ + if err := model.BindExternalAccount(ctx, &model.ExternalAccount{ AuthSourceID: source.ID, UserID: user.ID, ExternalID: userInfo.Sub, diff --git a/internal/apps/risk_control/middleware.go b/internal/apps/risk_control/middleware.go index 010e1320..1cf885d1 100644 --- a/internal/apps/risk_control/middleware.go +++ b/internal/apps/risk_control/middleware.go @@ -60,6 +60,14 @@ func RiskControlMiddleware() gin.HandlerFunc { } } + const maxHTTPStatus = 999 + status := c.Writer.Status() + if status < 0 { + status = 0 + } else if status > maxHTTPStatus { + status = maxHTTPStatus + } + logItem := &UserAccessLog{ ID: idgen.NextUint64ID(), UserID: userObj.ID, // 直接从 Context 获取已登录用户ID,避免数据库查询 @@ -68,7 +76,7 @@ func RiskControlMiddleware() gin.HandlerFunc { IP: c.ClientIP(), UserAgent: c.Request.UserAgent(), Headers: headersStr, - Status: int32(c.Writer.Status()), + Status: int32(status), Latency: latency, CreatedAt: time.Now(), } diff --git a/internal/apps/upload/routers.go b/internal/apps/upload/routers.go index e9549b0a..9a0cb4a8 100644 --- a/internal/apps/upload/routers.go +++ b/internal/apps/upload/routers.go @@ -499,7 +499,7 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User, if err := db.DB(ctx).Create(&newUpload).Error; err != nil { c.JSON(http.StatusOK, util.Err(ErrSaveUploadRecordFailed)) - return true, nil + return true, err } logger.InfoF(ctx, "文件触发秒传成功! ID: %d, Path: %s", id, existing.FilePath) diff --git a/internal/apps/user/errs.go b/internal/apps/user/errs.go index 006e1874..9f61b5d9 100644 --- a/internal/apps/user/errs.go +++ b/internal/apps/user/errs.go @@ -7,21 +7,21 @@ package user const ( errBindParamsFailed = "参数绑定失败" errInvalidParams = "无效的参数" - errPasswordLoginDisabled = "管理员关闭了密码登录" - errUsernameOrPasswordWrong = "用户名或密码错误" + errPasswordLoginDisabled = "管理员关闭了密码登录" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errUsernameOrPasswordWrong = "用户名或密码错误" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errLoginEmailMissing = "该账号未绑定邮箱,请联系管理员绑定邮箱后再登录" errNeedEmailCodePrefix = "need_email_code:" errEmailCodeInvalidOrExpired = "验证码错误或已过期" - errPasswordUpgradeFailed = "升级密码安全算法失败,请重试" + errPasswordUpgradeFailed = "升级密码安全算法失败,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errSaveSessionFailed = "无法保存会话信息,请重试" errRegistrationDisabled = "管理员关闭了注册" - errPasswordTooShort = "密码长度不能少于 8 位" + errPasswordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errEmailOrCodeRequired = "邮箱或验证码未填写" - errNewPasswordTooShort = "新密码长度不能少于 8 位" + errNewPasswordTooShort = "新密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errLoginRequired = "请先登录" errUserNotFound = "未找到该用户" - errOldPasswordIncorrect = "原密码不正确" - errPasswordEncryptFailed = "密码加密失败,请重试" + errOldPasswordIncorrect = "原密码不正确" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errPasswordEncryptFailed = "密码加密失败,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errEmailRequired = "邮箱地址不能为空" errUnsupportedEmailScene = "不支持的验证场景" errEmailAlreadyRegistered = "该邮箱已被注册" @@ -31,11 +31,11 @@ const ( errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w" errGenerateEmailCodeFailed = "生成验证码失败,请重试" errDispatchEmailTaskFailed = "投递验证邮件发送任务失败,请重试" - errTokenNameRequired = "令牌名称不能为空" - errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" - errGenerateTokenFailed = "生成令牌失败" - errInvalidTokenID = "无效的令牌ID" - errTokenNotFoundOrForbidden = "令牌不存在或无权操作" + errTokenNameRequired = "令牌名称不能为空" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errGenerateTokenFailed = "生成令牌失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errInvalidTokenID = "无效的令牌ID" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errTokenNotFoundOrForbidden = "令牌不存在或无权操作" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errTaskPayloadRequired = "任务参数不能为空" errInvalidJSONFormat = "无效的 JSON 格式: %w" errEmailTaskFieldsRequired = "to、subject、body 不能为空" diff --git a/internal/apps/user/logics.go b/internal/apps/user/logics.go index 2fb42954..a6450b54 100644 --- a/internal/apps/user/logics.go +++ b/internal/apps/user/logics.go @@ -28,16 +28,16 @@ type sendEmailCodeRequest struct { Scene string `json:"scene" binding:"required"` } -func isEmailLoginVerificationEnabled() bool { - enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyEmailLoginVerificationEnabled) +func isEmailLoginVerificationEnabled(ctx context.Context) bool { + enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled) if err != nil { return false } return enabled } -func isEmailRegisterVerificationEnabled() bool { - enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyEmailRegisterVerificationEnabled) +func isEmailRegisterVerificationEnabled(ctx context.Context) bool { + enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled) if err != nil { return false } @@ -215,7 +215,7 @@ func SendEmailCode(c *gin.Context) { } func validateRegisterEmailVerification(ctx context.Context, req *registerRequest) error { - if !isEmailRegisterVerificationEnabled() { + if !isEmailRegisterVerificationEnabled(ctx) { return nil } if req.Email == "" || req.Code == "" { @@ -228,7 +228,7 @@ func validateRegisterEmailVerification(ctx context.Context, req *registerRequest } // completePendingOAuthBinding 完成登录后的 OAuth 待绑定绑定流程 -func completePendingOAuthBinding(session sessions.Session, user *model.User) { +func completePendingOAuthBinding(ctx context.Context, session sessions.Session, user *model.User) { pendingSourceID := session.Get(oauth.PendingOAuthSourceIDKey) pendingExternalID := session.Get(oauth.PendingOAuthExternalIDKey) pendingExternalUsername := session.Get(oauth.PendingOAuthExternalUsernameKey) @@ -243,16 +243,20 @@ func completePendingOAuthBinding(session sessions.Session, user *model.User) { case uint64: sourceID = v case int: - sourceID = uint64(v) + if v >= 0 { + sourceID = uint64(v) + } case float64: - sourceID = uint64(v) + if v >= 0 && v <= 18446744073709551615.0 { + sourceID = uint64(v) + } } externalID, _ := pendingExternalID.(string) externalUsername, _ := pendingExternalUsername.(string) email, _ := pendingEmail.(string) if sourceID != 0 && externalID != "" { - _ = model.BindExternalAccount(&model.ExternalAccount{ + _ = model.BindExternalAccount(ctx, &model.ExternalAccount{ AuthSourceID: sourceID, UserID: user.ID, ExternalID: externalID, diff --git a/internal/apps/user/routers.go b/internal/apps/user/routers.go index 0e4f0d73..279b43be 100644 --- a/internal/apps/user/routers.go +++ b/internal/apps/user/routers.go @@ -114,7 +114,7 @@ func Login(c *gin.Context) { return } - if isEmailLoginVerificationEnabled() { + if isEmailLoginVerificationEnabled(ctx) { if emailErr := handleLoginEmailVerification(ctx, c, &req, &user); emailErr != nil { return } @@ -140,7 +140,7 @@ func Login(c *gin.Context) { } // 检查是否有未完成 of OAuth/OIDC 绑定 - completePendingOAuthBinding(session, &user) + completePendingOAuthBinding(ctx, session, &user) c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, needChangePassword))) } diff --git a/internal/apps/user/tasks.go b/internal/apps/user/tasks.go index efb3a149..178e482f 100644 --- a/internal/apps/user/tasks.go +++ b/internal/apps/user/tasks.go @@ -101,7 +101,7 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T task.AppendLog(ctx, "连接 SMTP 服务器: %s:%d, 用户名: %s", smtpHost, smtpPort, smtpUsername) // 调用 SendMailHTML 执行邮件发送,这里会有 5s 拨号超时和 10s 读写限制 - err = mail.SendMailHTML(cfg, req.To, req.Subject, req.Body) + err = mail.SendMailHTML(ctx, cfg, req.To, req.Subject, req.Body) if err != nil { task.AppendLog(ctx, "邮件发送失败: %v", err) return nil, fmt.Errorf(errSendMailFailed, err) diff --git a/internal/config/config.go b/internal/config/config.go index a8d66241..fb5326a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -70,7 +70,7 @@ func init() { if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { // 文件存在但读取/解析失败 - if _, statErr := os.Stat(configPath); statErr == nil { + if _, statErr := os.Stat(configPath); statErr == nil { //nolint:gosec // configPath is loaded from CONFIG_PATH environment variable log.Fatalf("[Config] read config failed: %v\n", err) } } diff --git a/internal/db/idgen/snowflake.go b/internal/db/idgen/snowflake.go index 51ceb053..8f67fc87 100644 --- a/internal/db/idgen/snowflake.go +++ b/internal/db/idgen/snowflake.go @@ -31,5 +31,9 @@ func init() { // NextUint64ID 生成下一个分布式唯一 ID func NextUint64ID() uint64 { - return uint64(node.Generate().Int64()) + val := node.Generate().Int64() + if val < 0 { + return 0 + } + return uint64(val) } diff --git a/internal/model/auth_source.go b/internal/model/auth_source.go index 95687e99..3a2c28b6 100644 --- a/internal/model/auth_source.go +++ b/internal/model/auth_source.go @@ -221,16 +221,16 @@ func DeleteAuthSource(id uint64) error { } // FindExternalAccount 查找外部帐号绑定记录 -func FindExternalAccount(sourceID uint64, externalID string) (*ExternalAccount, error) { +func FindExternalAccount(ctx context.Context, sourceID uint64, externalID string) (*ExternalAccount, error) { var account ExternalAccount - if err := db.DB(context.Background()).Where("auth_source_id = ? AND external_id = ?", sourceID, externalID).First(&account).Error; err != nil { + if err := db.DB(ctx).Where("auth_source_id = ? AND external_id = ?", sourceID, externalID).First(&account).Error; err != nil { return nil, err } return &account, nil } // BindExternalAccount 绑定外部帐号(已存在时更新用户名和邮箱) -func BindExternalAccount(account *ExternalAccount) error { +func BindExternalAccount(ctx context.Context, account *ExternalAccount) error { if account.UserID == 0 || strings.TrimSpace(account.ExternalID) == "" { return errors.New(errExternalAccountBindingIncomplete) } @@ -238,7 +238,7 @@ func BindExternalAccount(account *ExternalAccount) error { account.ExternalUsername = strings.TrimSpace(account.ExternalUsername) account.Email = strings.TrimSpace(account.Email) - return db.DB(context.Background()).Transaction(func(tx *gorm.DB) error { + return db.DB(ctx).Transaction(func(tx *gorm.DB) error { var current ExternalAccount err := tx.Where("auth_source_id = ? AND external_id = ?", account.AuthSourceID, account.ExternalID).First(¤t).Error if err == nil { diff --git a/internal/model/errs.go b/internal/model/errs.go index d72a5b1d..6c2a64ee 100644 --- a/internal/model/errs.go +++ b/internal/model/errs.go @@ -21,7 +21,7 @@ const ( errAuthSourceNameInvalid = "认证源名称只能包含字母、数字、短横线或下划线,且必须以字母或数字开头" errAuthSourceTypeUnsupported = "认证源类型仅支持 oidc" errAuthSourceDiscoveryURLRequired = "OIDC 认证源必须配置 Discovery URL" - errAuthSourceClientCredentialsRequired = "启用认证源前必须配置 Client ID 和 Client Secret" + errAuthSourceClientCredentialsRequired = "启用认证源前必须配置 Client ID 和 Client Secret" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errAuthSourceIDRequired = "认证源 ID 不能为空" errExternalAccountBindingIncomplete = "外部账号绑定信息不完整" errExternalAccountAlreadyBoundToAnother = "该外部账号已绑定到其他用户" diff --git a/internal/model/system_configs.go b/internal/model/system_configs.go index 0e6c3dc3..fe6dd375 100644 --- a/internal/model/system_configs.go +++ b/internal/model/system_configs.go @@ -26,14 +26,14 @@ const ( ConfigKeyRegistrationEnabled = "registration_enabled" // 是否允许注册 ConfigKeyPasswordRegisterEnabled = "password_register_enabled" // 是否允许密码注册 ConfigKeyOIDCLoginEnabled = "oidc_login_enabled" // 是否允许 OIDC 登录 - ConfigKeyMaxAPIKeysPerUser = "max_api_keys_per_user" // 每个用户最大 API Key 数量 + ConfigKeyMaxAPIKeysPerUser = "max_api_keys_per_user" //nolint:gosec // false positive: config key name. 每个用户最大 API Key 数量 ConfigKeyCapLoginEnabled = "cap_login_enabled" // 是否启用登录人机验证 ConfigKeyCapAutoSolve = "cap_auto_solve" // 打开页面后是否自动开始计算(false 则需用户手动点击) ConfigKeyCapChallengeCount = "cap_challenge_count" // 客户端需求解的 PoW 难题总数,默认 1,推荐 1~5 ConfigKeyCapChallengeSize = "cap_challenge_size" // 人机验证盐值长度 ConfigKeyCapChallengeDifficulty = "cap_challenge_difficulty" // 人机验证 PoW 难度(目标前缀长度) ConfigKeyCapChallengeTTL = "cap_challenge_ttl_seconds" // 人机验证难题有效时间(秒) - ConfigKeyCapTokenTTL = "cap_token_ttl_seconds" // 人机验证兑换凭证有效时间(秒) + ConfigKeyCapTokenTTL = "cap_token_ttl_seconds" //nolint:gosec // false positive: config key name. 人机验证兑换凭证有效时间(秒) ConfigKeyServerAddress = "server_address" // 服务器地址 ConfigKeySMTPHost = "smtp_host" // SMTP 服务器地址 ConfigKeySMTPPort = "smtp_port" // SMTP 端口 diff --git a/internal/router/router.go b/internal/router/router.go index 7816feb7..ea99e92e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -96,6 +96,40 @@ func Serve() { // 补充中间件 r.Use(otelgin.Middleware(config.Config.App.AppName), loggerMiddleware(), risk_control.RiskControlMiddleware()) + registerRoutes(r) + + srv := &http.Server{ + Addr: config.Config.App.Addr, + Handler: r, + ReadHeaderTimeout: 10 * time.Second, + } + + go func() { + log.Printf("[API] server starting on %s\n", config.Config.App.Addr) + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("[API] server failed: %v\n", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second) + + otel_trace.Shutdown(shutdownCtx) + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Printf("[API] server forced to shutdown: %v\n", err) + cancel() + os.Exit(1) + } + cancel() + + log.Println("[API] server exited") +} + +func registerRoutes(r *gin.Engine) { // Serve files by ID r.GET("/f/:id", upload.ServeFileByID) @@ -251,33 +285,4 @@ func Serve() { // 注册前端静态路由(当启用 embed_frontend 编译标签时) registerFrontend(r) - - srv := &http.Server{ - Addr: config.Config.App.Addr, - Handler: r, - } - - go func() { - log.Printf("[API] server starting on %s\n", config.Config.App.Addr) - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("[API] server failed: %v\n", err) - } - }() - - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Config.App.GracefulShutdownTimeout)*time.Second) - - otel_trace.Shutdown(shutdownCtx) - - if err := srv.Shutdown(shutdownCtx); err != nil { - log.Printf("[API] server forced to shutdown: %v\n", err) - cancel() - os.Exit(1) - } - cancel() - - log.Println("[API] server exited") } diff --git a/internal/storage/cache.go b/internal/storage/cache.go index 2d011db7..07838331 100644 --- a/internal/storage/cache.go +++ b/internal/storage/cache.go @@ -101,7 +101,7 @@ func getLocalCacheFile(ctx context.Context, localPath, metaPath string) (*Object defer span.End() // 尝试打开本地缓存文件 - file, err := os.Open(localPath) + file, err := os.Open(localPath) //nolint:gosec // localPath is internally managed cache path if err == nil { defer func() { _ = file.Close() }() } @@ -118,7 +118,7 @@ func getLocalCacheFile(ctx context.Context, localPath, metaPath string) (*Object } // 读取元信息 - metaData, err := os.ReadFile(metaPath) + metaData, err := os.ReadFile(metaPath) //nolint:gosec // metaPath is internally managed cache path // 文件不存在 if err != nil && os.IsNotExist(err) { diff --git a/internal/util/cap/manager.go b/internal/util/cap/manager.go index 16e49e74..6178c31d 100644 --- a/internal/util/cap/manager.go +++ b/internal/util/cap/manager.go @@ -94,7 +94,7 @@ func (m *Manager) Redeem(ctx context.Context, token string, solutions []int, sco // TTL is set to the challenge's remaining lifetime so the slot auto-expires. payload, err := VerifyChallengeSolutions(token, solutions, m.conf.Secret, scope) if err != nil { - return &RedeemResponse{Success: false, Error: err.Error()}, nil + return &RedeemResponse{Success: false, Error: err.Error()}, nil //nolint:nilerr // expected behavior: validation error is returned as response, not system error } // Calculate remaining lifetime of the challenge JWT for the nonce TTL. @@ -174,7 +174,7 @@ func (m *Manager) VerifyToken(ctx context.Context, token string, expectedScope s expNano, err := strconv.ParseInt(valParts[0], 10, 64) if err != nil { - return false, nil + return false, nil //nolint:nilerr // expected behavior: invalid format is treated as validation failure, not system error } tokenScope := valParts[1] diff --git a/internal/util/http_clients.go b/internal/util/http_clients.go index 1a78f53c..637897b6 100644 --- a/internal/util/http_clients.go +++ b/internal/util/http_clients.go @@ -56,7 +56,7 @@ func Request(ctx context.Context, method, url string, body io.Reader, headers, c } for key, value := range cookies { - req.AddCookie(&http.Cookie{Name: key, Value: value}) + req.AddCookie(&http.Cookie{Name: key, Value: value}) //nolint:gosec // client-side cookies do not require server attributes (Secure/HttpOnly) } for key, value := range headers { diff --git a/internal/util/mail/errs.go b/internal/util/mail/errs.go index 87a6367e..2c930984 100644 --- a/internal/util/mail/errs.go +++ b/internal/util/mail/errs.go @@ -11,6 +11,6 @@ const ( errSMTPMailCommandFailed = "smtp mail command failed: %w" errSMTPRcptCommandFailed = "smtp rcpt command failed: %w" errSMTPDataCommandFailed = "smtp data command failed: %w" - errSMTPWritingBodyFailed = "smtp writing body failed: %w" + errSMTPWritingBodyFailed = "smtp writing body failed: %w" //nolint:gosec // false positive: this is an error message, not hardcoded credentials errSendMailFailed = "send mail failed: %w" ) diff --git a/internal/util/mail/mail.go b/internal/util/mail/mail.go index 1037dbab..0c117ec8 100644 --- a/internal/util/mail/mail.go +++ b/internal/util/mail/mail.go @@ -6,6 +6,7 @@ package mail import ( "bytes" + "context" "crypto/tls" "fmt" "net" @@ -29,12 +30,12 @@ type Config struct { } // SendMail sends an HTML email using the provided config and message details -func SendMail(cfg Config, to string, subject, body string) error { - return SendMailHTML(cfg, to, subject, body) +func SendMail(ctx context.Context, cfg Config, to string, subject, body string) error { + return SendMailHTML(ctx, cfg, to, subject, body) } // SendMailHTML sends an HTML format email -func SendMailHTML(cfg Config, to string, subject, body string) error { +func SendMailHTML(ctx context.Context, cfg Config, to string, subject, body string) error { addr := net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)) // Header & MIME settings for HTML email @@ -55,7 +56,7 @@ func SendMailHTML(cfg Config, to string, subject, body string) error { // If using SSL port 465, we connection via TLS dial if cfg.Port == smtpSSLPort { - return sendMailViaSSL(addr, auth, cfg, to, message) + return sendMailViaSSL(ctx, addr, auth, cfg, to, message) } // For standard port (587 / 25), use smtp.SendMail directly (handles STARTTLS automatically if server supports it) @@ -68,13 +69,17 @@ func SendMailHTML(cfg Config, to string, subject, body string) error { } // sendMailViaSSL 通过 TLS 直接连接 SMTP SSL 端口发送邮件 -func sendMailViaSSL(addr string, auth smtp.Auth, cfg Config, to, message string) error { +func sendMailViaSSL(ctx context.Context, addr string, auth smtp.Auth, cfg Config, to, message string) error { tlsConfig := &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates ServerName: cfg.Host, } dialer := &net.Dialer{Timeout: smtpDialTimeout} - conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig) + tlsDialer := &tls.Dialer{ + NetDialer: dialer, + Config: tlsConfig, + } + conn, err := tlsDialer.DialContext(ctx, "tcp", addr) if err != nil { return fmt.Errorf(errDialTLSFailed, err) } @@ -111,7 +116,7 @@ func sendMailViaSSL(addr string, auth smtp.Auth, cfg Config, to, message string) } // SendMailWithLog sends a test email and records a detailed SMTP connection log -func SendMailWithLog(cfg Config, to string, subject, body string) (string, error) { +func SendMailWithLog(ctx context.Context, cfg Config, to string, subject, body string) (string, error) { var logBuf bytes.Buffer logLine := func(dir string, format string, args ...interface{}) { fmt.Fprintf(&logBuf, "[%s] %s\n", dir, fmt.Sprintf(format, args...)) @@ -125,12 +130,16 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error dialer := &net.Dialer{Timeout: smtpDialTimeout} if cfg.Port == smtpSSLPort { tlsConfig := &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates ServerName: cfg.Host, } - conn, err = tls.DialWithDialer(dialer, "tcp", addr, tlsConfig) + tlsDialer := &tls.Dialer{ + NetDialer: dialer, + Config: tlsConfig, + } + conn, err = tlsDialer.DialContext(ctx, "tcp", addr) } else { - conn, err = dialer.Dial("tcp", addr) + conn, err = dialer.DialContext(ctx, "tcp", addr) } if err != nil { logLine("Error", "Connection failed: %v", err) @@ -154,7 +163,7 @@ func SendMailWithLog(cfg Config, to string, subject, body string) (string, error if ok, _ := client.Extension("STARTTLS"); ok { logLine("C", "STARTTLS") tlsConfig := &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: true, //nolint:gosec // SMTP servers might use self-signed certificates ServerName: cfg.Host, } if err = client.StartTLS(tlsConfig); err != nil { diff --git a/internal/util/mail/mail_test.go b/internal/util/mail/mail_test.go index b3d91c6b..2fcc60be 100644 --- a/internal/util/mail/mail_test.go +++ b/internal/util/mail/mail_test.go @@ -6,6 +6,7 @@ package mail import ( "bufio" + "context" "net" "net/textproto" "testing" @@ -84,7 +85,7 @@ func TestSendMailMock(t *testing.T) { Password: "password", } - err = SendMail(cfg, "recipient@example.com", "Test Subject", "

Test Body

") + err = SendMail(context.Background(), cfg, "recipient@example.com", "Test Subject", "

Test Body

") if err != nil { t.Errorf("failed to send mail: %v", err) }