新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
136 lines
3.7 KiB
Go
136 lines
3.7 KiB
Go
package listing
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
_ "embed"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"hfb_sys/backend/pkg/response"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
//go:embed assets/default_upload_screenshot.webp
|
|
var defaultUploadScreenshotWebP []byte
|
|
|
|
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 (
|
|
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 {
|
|
if !externalUploadIPAllowed(c.ClientIP(), h.externalUploadAllowedIPs) {
|
|
return errExternalUploadForbiddenIP
|
|
}
|
|
secret := strings.TrimSpace(h.externalUploadSecret)
|
|
if secret == "" {
|
|
return nil
|
|
}
|
|
timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp"))
|
|
signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature"))
|
|
if timestamp == "" && signature == "" {
|
|
return nil
|
|
}
|
|
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) {
|
|
response.RecordError(c, err)
|
|
switch err {
|
|
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/webp")
|
|
c.Header("Cache-Control", "public, max-age=604800")
|
|
c.Header("Content-Length", strconv.Itoa(len(defaultUploadScreenshotWebP)))
|
|
c.Data(http.StatusOK, "image/webp", defaultUploadScreenshotWebP)
|
|
}
|