兼容开放导入未签名请求
This commit is contained in:
@@ -56,8 +56,8 @@ REALNAME_CLOUDMARKET_APPCODE=
|
|||||||
# 生成方式:openssl rand -hex 16
|
# 生成方式:openssl rand -hex 16
|
||||||
PAYMENT_CONFIG_ENCRYPTION_KEY=
|
PAYMENT_CONFIG_ENCRYPTION_KEY=
|
||||||
|
|
||||||
# 开放导入接口签名密钥;留空时 /api/open/listing-uploads 不可用。
|
# 开放导入接口可选签名密钥;留空或请求未带签名时按旧方式导入。
|
||||||
# 请求需携带 X-HFB-Timestamp 和 X-HFB-Signature。
|
# 若调用方携带 X-HFB-Timestamp 和 X-HFB-Signature,则会校验 HMAC 签名。
|
||||||
EXTERNAL_UPLOAD_SECRET=
|
EXTERNAL_UPLOAD_SECRET=
|
||||||
# 可选:逗号分隔的 IP 或 CIDR 白名单,例如 127.0.0.1,10.0.0.0/8。
|
# 可选:逗号分隔的 IP 或 CIDR 白名单,例如 127.0.0.1,10.0.0.0/8。
|
||||||
EXTERNAL_UPLOAD_ALLOWED_IPS=
|
EXTERNAL_UPLOAD_ALLOWED_IPS=
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ REALNAME_CLOUDMARKET_APPCODE=
|
|||||||
# 警告:此密钥一旦设置不要更改,否则已有配置无法解密
|
# 警告:此密钥一旦设置不要更改,否则已有配置无法解密
|
||||||
PAYMENT_CONFIG_ENCRYPTION_KEY=change-to-32-byte-encryption-key
|
PAYMENT_CONFIG_ENCRYPTION_KEY=change-to-32-byte-encryption-key
|
||||||
|
|
||||||
# 开放导入接口签名密钥,生产环境必填;生成方式:openssl rand -hex 32
|
# 开放导入接口可选签名密钥;内部调用方暂不签名时可留空。
|
||||||
EXTERNAL_UPLOAD_SECRET=change-to-a-long-random-upload-secret
|
# 若启用签名,生成方式:openssl rand -hex 32
|
||||||
|
EXTERNAL_UPLOAD_SECRET=
|
||||||
# 可选:逗号分隔的 IP 或 CIDR 白名单,例如 203.0.113.10,10.0.0.0/8。
|
# 可选:逗号分隔的 IP 或 CIDR 白名单,例如 203.0.113.10,10.0.0.0/8。
|
||||||
EXTERNAL_UPLOAD_ALLOWED_IPS=
|
EXTERNAL_UPLOAD_ALLOWED_IPS=
|
||||||
|
|||||||
@@ -120,9 +120,6 @@ func (c Config) ValidateProductionSecurity() error {
|
|||||||
if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) {
|
if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) {
|
||||||
return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production")
|
return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(c.ExternalUploadSecret) == "" || isPlaceholder(c.ExternalUploadSecret) {
|
|
||||||
return errors.New("EXTERNAL_UPLOAD_SECRET is required in production")
|
|
||||||
}
|
|
||||||
if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) {
|
if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) {
|
||||||
return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production")
|
return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ func (h *Handler) ImportExternalUpload(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errExternalUploadSecretMissing = errors.New("external upload secret missing")
|
|
||||||
errExternalUploadForbiddenIP = errors.New("external upload forbidden ip")
|
errExternalUploadForbiddenIP = errors.New("external upload forbidden ip")
|
||||||
errExternalUploadTimestamp = errors.New("external upload timestamp invalid")
|
errExternalUploadTimestamp = errors.New("external upload timestamp invalid")
|
||||||
errExternalUploadSignature = errors.New("external upload signature invalid")
|
errExternalUploadSignature = errors.New("external upload signature invalid")
|
||||||
@@ -56,15 +55,18 @@ var (
|
|||||||
const externalUploadMaxClockSkew = 5 * time.Minute
|
const externalUploadMaxClockSkew = 5 * time.Minute
|
||||||
|
|
||||||
func (h *Handler) verifyExternalUpload(c *gin.Context, raw []byte) error {
|
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) {
|
if !externalUploadIPAllowed(c.ClientIP(), h.externalUploadAllowedIPs) {
|
||||||
return errExternalUploadForbiddenIP
|
return errExternalUploadForbiddenIP
|
||||||
}
|
}
|
||||||
|
secret := strings.TrimSpace(h.externalUploadSecret)
|
||||||
|
if secret == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp"))
|
timestamp := strings.TrimSpace(c.GetHeader("X-HFB-Timestamp"))
|
||||||
signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature"))
|
signature := strings.TrimSpace(c.GetHeader("X-HFB-Signature"))
|
||||||
|
if timestamp == "" && signature == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if timestamp == "" || signature == "" {
|
if timestamp == "" || signature == "" {
|
||||||
return errExternalUploadSignature
|
return errExternalUploadSignature
|
||||||
}
|
}
|
||||||
@@ -111,8 +113,6 @@ func externalUploadIPAllowed(clientIP string, allowed []string) bool {
|
|||||||
|
|
||||||
func writeExternalUploadAuthError(c *gin.Context, err error) {
|
func writeExternalUploadAuthError(c *gin.Context, err error) {
|
||||||
switch err {
|
switch err {
|
||||||
case errExternalUploadSecretMissing:
|
|
||||||
response.ServiceUnavailable(c, "开放导入签名密钥未配置")
|
|
||||||
case errExternalUploadForbiddenIP:
|
case errExternalUploadForbiddenIP:
|
||||||
response.Error(c, http.StatusForbidden, "forbidden", "来源 IP 不允许访问")
|
response.Error(c, http.StatusForbidden, "forbidden", "来源 IP 不允许访问")
|
||||||
case errExternalUploadTimestamp:
|
case errExternalUploadTimestamp:
|
||||||
|
|||||||
@@ -30,6 +30,28 @@ func TestVerifyExternalUploadSignature(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifyExternalUploadAllowsUnsignedRequest(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request = httptest.NewRequest("POST", "/api/open/listing-uploads", nil)
|
||||||
|
|
||||||
|
handler := &Handler{}
|
||||||
|
if err := handler.verifyExternalUpload(c, []byte(`{}`)); err != nil {
|
||||||
|
t.Fatalf("verifyExternalUpload() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyExternalUploadAllowsUnsignedRequestWithSecret(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request = httptest.NewRequest("POST", "/api/open/listing-uploads", nil)
|
||||||
|
|
||||||
|
handler := &Handler{externalUploadSecret: "test-upload-secret"}
|
||||||
|
if err := handler.verifyExternalUpload(c, []byte(`{}`)); err != nil {
|
||||||
|
t.Fatalf("verifyExternalUpload() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVerifyExternalUploadRejectsBadSignature(t *testing.T) {
|
func TestVerifyExternalUploadRejectsBadSignature(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||||
|
|||||||
Reference in New Issue
Block a user