package listing import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "net" "net/http" "strconv" "strings" "time" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" ) func (h *Handler) ImportExternalUpload(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxExternalUploadBodyBytes) raw, err := io.ReadAll(c.Request.Body) if err != nil { response.BadRequest(c, "上传内容过大或读取失败") return } if err := h.verifyExternalUpload(c, raw); err != nil { writeExternalUploadAuthError(c, err) return } var req ExternalUploadRequest if err := json.Unmarshal(raw, &req); err != nil { response.BadRequest(c, "上传 JSON 格式不正确") return } result, err := h.service.ImportExternalUpload(c.Request.Context(), req, ExternalUploadMeta{ IP: c.ClientIP(), UserAgent: c.GetHeader("User-Agent"), RawPayload: raw, }) if err != nil { writeListingError(c, err) return } response.Created(c, result) } var ( errExternalUploadSecretMissing = errors.New("external upload secret missing") errExternalUploadForbiddenIP = errors.New("external upload forbidden ip") errExternalUploadTimestamp = errors.New("external upload timestamp invalid") errExternalUploadSignature = errors.New("external upload signature invalid") ) const externalUploadMaxClockSkew = 5 * time.Minute func (h *Handler) verifyExternalUpload(c *gin.Context, raw []byte) error { secret := strings.TrimSpace(h.externalUploadSecret) if secret == "" { return errExternalUploadSecretMissing } if !externalUploadIPAllowed(c.ClientIP(), h.externalUploadAllowedIPs) { return errExternalUploadForbiddenIP } timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp")) signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature")) if timestamp == "" || signature == "" { return errExternalUploadSignature } ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return errExternalUploadTimestamp } requestTime := time.Unix(ts, 0) if time.Since(requestTime) > externalUploadMaxClockSkew || time.Until(requestTime) > externalUploadMaxClockSkew { return errExternalUploadTimestamp } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp)) mac.Write([]byte(".")) mac.Write(raw) expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(strings.ToLower(signature)), []byte(expected)) { return errExternalUploadSignature } return nil } func externalUploadIPAllowed(clientIP string, allowed []string) bool { if len(allowed) == 0 { return true } parsedIP := net.ParseIP(clientIP) for _, raw := range allowed { item := strings.TrimSpace(raw) if item == "" { continue } if parsedIP != nil { if _, network, err := net.ParseCIDR(item); err == nil && network.Contains(parsedIP) { return true } } if item == clientIP { return true } } return false } func writeExternalUploadAuthError(c *gin.Context, err error) { switch err { case errExternalUploadSecretMissing: response.ServiceUnavailable(c, "开放导入签名密钥未配置") case errExternalUploadForbiddenIP: response.Error(c, http.StatusForbidden, "forbidden", "来源 IP 不允许访问") case errExternalUploadTimestamp: response.Error(c, http.StatusUnauthorized, "invalid_timestamp", "请求时间戳无效") default: response.Error(c, http.StatusUnauthorized, "invalid_signature", "请求签名无效") } } func (h *Handler) DefaultUploadScreenshot(c *gin.Context) { c.Header("Content-Type", "image/svg+xml; charset=utf-8") c.Header("Cache-Control", "public, max-age=86400") c.String(http.StatusOK, `账号资料默认图开放接口上传,待后台补充审核`) }