Merge pull request #2645 from lyen1688/fix/trusted-forwarded-ip-acl
PR:为 API Key IP 白/黑名单增加可配置的反代真实 IP 判断
This commit is contained in:
commit
d3c4e50753
@ -9,6 +9,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
@ -573,11 +574,35 @@ type CORSConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SecurityConfig struct {
|
type SecurityConfig struct {
|
||||||
URLAllowlist URLAllowlistConfig `mapstructure:"url_allowlist"`
|
URLAllowlist URLAllowlistConfig `mapstructure:"url_allowlist"`
|
||||||
ResponseHeaders ResponseHeaderConfig `mapstructure:"response_headers"`
|
ResponseHeaders ResponseHeaderConfig `mapstructure:"response_headers"`
|
||||||
CSP CSPConfig `mapstructure:"csp"`
|
CSP CSPConfig `mapstructure:"csp"`
|
||||||
ProxyFallback ProxyFallbackConfig `mapstructure:"proxy_fallback"`
|
ProxyFallback ProxyFallbackConfig `mapstructure:"proxy_fallback"`
|
||||||
ProxyProbe ProxyProbeConfig `mapstructure:"proxy_probe"`
|
ProxyProbe ProxyProbeConfig `mapstructure:"proxy_probe"`
|
||||||
|
TrustForwardedIPForAPIKeyACL bool `mapstructure:"trust_forwarded_ip_for_api_key_acl"`
|
||||||
|
trustForwardedIPForAPIKeyACLLive *atomic.Bool `mapstructure:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) TrustForwardedIPForAPIKeyACL() bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
live := c.Security.trustForwardedIPForAPIKeyACLLive
|
||||||
|
if live == nil {
|
||||||
|
return c.Security.TrustForwardedIPForAPIKeyACL
|
||||||
|
}
|
||||||
|
return live.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetTrustForwardedIPForAPIKeyACL(enabled bool) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Security.TrustForwardedIPForAPIKeyACL = enabled
|
||||||
|
if c.Security.trustForwardedIPForAPIKeyACLLive == nil {
|
||||||
|
c.Security.trustForwardedIPForAPIKeyACLLive = &atomic.Bool{}
|
||||||
|
}
|
||||||
|
c.Security.trustForwardedIPForAPIKeyACLLive.Store(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
type URLAllowlistConfig struct {
|
type URLAllowlistConfig struct {
|
||||||
@ -1363,6 +1388,7 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
|
|||||||
cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
|
cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
|
||||||
cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
|
cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
|
||||||
cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
|
cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
|
||||||
|
cfg.SetTrustForwardedIPForAPIKeyACL(cfg.Security.TrustForwardedIPForAPIKeyACL)
|
||||||
cfg.Log.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
|
cfg.Log.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
|
||||||
cfg.Log.Format = strings.ToLower(strings.TrimSpace(cfg.Log.Format))
|
cfg.Log.Format = strings.ToLower(strings.TrimSpace(cfg.Log.Format))
|
||||||
cfg.Log.ServiceName = strings.TrimSpace(cfg.Log.ServiceName)
|
cfg.Log.ServiceName = strings.TrimSpace(cfg.Log.ServiceName)
|
||||||
@ -1507,6 +1533,7 @@ func setDefaults() {
|
|||||||
viper.SetDefault("security.csp.enabled", true)
|
viper.SetDefault("security.csp.enabled", true)
|
||||||
viper.SetDefault("security.csp.policy", DefaultCSPPolicy)
|
viper.SetDefault("security.csp.policy", DefaultCSPPolicy)
|
||||||
viper.SetDefault("security.proxy_probe.insecure_skip_verify", false)
|
viper.SetDefault("security.proxy_probe.insecure_skip_verify", false)
|
||||||
|
viper.SetDefault("security.trust_forwarded_ip_for_api_key_acl", false)
|
||||||
|
|
||||||
// Security - disable direct fallback on proxy error
|
// Security - disable direct fallback on proxy error
|
||||||
viper.SetDefault("security.proxy_fallback.allow_direct_on_error", false)
|
viper.SetDefault("security.proxy_fallback.allow_direct_on_error", false)
|
||||||
|
|||||||
@ -142,6 +142,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|||||||
TurnstileEnabled: settings.TurnstileEnabled,
|
TurnstileEnabled: settings.TurnstileEnabled,
|
||||||
TurnstileSiteKey: settings.TurnstileSiteKey,
|
TurnstileSiteKey: settings.TurnstileSiteKey,
|
||||||
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
|
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
|
||||||
|
APIKeyACLTrustForwardedIP: settings.APIKeyACLTrustForwardedIP,
|
||||||
LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
|
LinuxDoConnectEnabled: settings.LinuxDoConnectEnabled,
|
||||||
LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: settings.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
|
LinuxDoConnectClientSecretConfigured: settings.LinuxDoConnectClientSecretConfigured,
|
||||||
@ -399,6 +400,9 @@ type UpdateSettingsRequest struct {
|
|||||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||||
TurnstileSecretKey string `json:"turnstile_secret_key"`
|
TurnstileSecretKey string `json:"turnstile_secret_key"`
|
||||||
|
|
||||||
|
// API Key IP 访问控制设置
|
||||||
|
APIKeyACLTrustForwardedIP *bool `json:"api_key_acl_trust_forwarded_ip"`
|
||||||
|
|
||||||
// LinuxDo Connect OAuth 登录
|
// LinuxDo Connect OAuth 登录
|
||||||
LinuxDoConnectEnabled bool `json:"linuxdo_connect_enabled"`
|
LinuxDoConnectEnabled bool `json:"linuxdo_connect_enabled"`
|
||||||
LinuxDoConnectClientID string `json:"linuxdo_connect_client_id"`
|
LinuxDoConnectClientID string `json:"linuxdo_connect_client_id"`
|
||||||
@ -1432,28 +1436,34 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
settings := &service.SystemSettings{
|
settings := &service.SystemSettings{
|
||||||
RegistrationEnabled: req.RegistrationEnabled,
|
RegistrationEnabled: req.RegistrationEnabled,
|
||||||
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
EmailVerifyEnabled: req.EmailVerifyEnabled,
|
||||||
RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
|
RegistrationEmailSuffixWhitelist: req.RegistrationEmailSuffixWhitelist,
|
||||||
PromoCodeEnabled: req.PromoCodeEnabled,
|
PromoCodeEnabled: req.PromoCodeEnabled,
|
||||||
PasswordResetEnabled: req.PasswordResetEnabled,
|
PasswordResetEnabled: req.PasswordResetEnabled,
|
||||||
FrontendURL: req.FrontendURL,
|
FrontendURL: req.FrontendURL,
|
||||||
InvitationCodeEnabled: req.InvitationCodeEnabled,
|
InvitationCodeEnabled: req.InvitationCodeEnabled,
|
||||||
TotpEnabled: req.TotpEnabled,
|
TotpEnabled: req.TotpEnabled,
|
||||||
LoginAgreementEnabled: req.LoginAgreementEnabled,
|
LoginAgreementEnabled: req.LoginAgreementEnabled,
|
||||||
LoginAgreementMode: loginAgreementMode,
|
LoginAgreementMode: loginAgreementMode,
|
||||||
LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
|
LoginAgreementUpdatedAt: loginAgreementUpdatedAt,
|
||||||
LoginAgreementDocuments: loginAgreementDocuments,
|
LoginAgreementDocuments: loginAgreementDocuments,
|
||||||
SMTPHost: req.SMTPHost,
|
SMTPHost: req.SMTPHost,
|
||||||
SMTPPort: req.SMTPPort,
|
SMTPPort: req.SMTPPort,
|
||||||
SMTPUsername: req.SMTPUsername,
|
SMTPUsername: req.SMTPUsername,
|
||||||
SMTPPassword: req.SMTPPassword,
|
SMTPPassword: req.SMTPPassword,
|
||||||
SMTPFrom: req.SMTPFrom,
|
SMTPFrom: req.SMTPFrom,
|
||||||
SMTPFromName: req.SMTPFromName,
|
SMTPFromName: req.SMTPFromName,
|
||||||
SMTPUseTLS: req.SMTPUseTLS,
|
SMTPUseTLS: req.SMTPUseTLS,
|
||||||
TurnstileEnabled: req.TurnstileEnabled,
|
TurnstileEnabled: req.TurnstileEnabled,
|
||||||
TurnstileSiteKey: req.TurnstileSiteKey,
|
TurnstileSiteKey: req.TurnstileSiteKey,
|
||||||
TurnstileSecretKey: req.TurnstileSecretKey,
|
TurnstileSecretKey: req.TurnstileSecretKey,
|
||||||
|
APIKeyACLTrustForwardedIP: func() bool {
|
||||||
|
if req.APIKeyACLTrustForwardedIP != nil {
|
||||||
|
return *req.APIKeyACLTrustForwardedIP
|
||||||
|
}
|
||||||
|
return previousSettings.APIKeyACLTrustForwardedIP
|
||||||
|
}(),
|
||||||
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
LinuxDoConnectEnabled: req.LinuxDoConnectEnabled,
|
||||||
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: req.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
LinuxDoConnectClientSecret: req.LinuxDoConnectClientSecret,
|
||||||
@ -1869,6 +1879,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|||||||
TurnstileEnabled: updatedSettings.TurnstileEnabled,
|
TurnstileEnabled: updatedSettings.TurnstileEnabled,
|
||||||
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
|
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
|
||||||
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
|
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
|
||||||
|
APIKeyACLTrustForwardedIP: updatedSettings.APIKeyACLTrustForwardedIP,
|
||||||
LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
|
LinuxDoConnectEnabled: updatedSettings.LinuxDoConnectEnabled,
|
||||||
LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
|
LinuxDoConnectClientID: updatedSettings.LinuxDoConnectClientID,
|
||||||
LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
|
LinuxDoConnectClientSecretConfigured: updatedSettings.LinuxDoConnectClientSecretConfigured,
|
||||||
@ -2145,6 +2156,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|||||||
if req.TurnstileSecretKey != "" {
|
if req.TurnstileSecretKey != "" {
|
||||||
changed = append(changed, "turnstile_secret_key")
|
changed = append(changed, "turnstile_secret_key")
|
||||||
}
|
}
|
||||||
|
if before.APIKeyACLTrustForwardedIP != after.APIKeyACLTrustForwardedIP {
|
||||||
|
changed = append(changed, "api_key_acl_trust_forwarded_ip")
|
||||||
|
}
|
||||||
if before.LinuxDoConnectEnabled != after.LinuxDoConnectEnabled {
|
if before.LinuxDoConnectEnabled != after.LinuxDoConnectEnabled {
|
||||||
changed = append(changed, "linuxdo_connect_enabled")
|
changed = append(changed, "linuxdo_connect_enabled")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,7 @@ type SystemSettings struct {
|
|||||||
TurnstileEnabled bool `json:"turnstile_enabled"`
|
TurnstileEnabled bool `json:"turnstile_enabled"`
|
||||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||||
TurnstileSecretKeyConfigured bool `json:"turnstile_secret_key_configured"`
|
TurnstileSecretKeyConfigured bool `json:"turnstile_secret_key_configured"`
|
||||||
|
APIKeyACLTrustForwardedIP bool `json:"api_key_acl_trust_forwarded_ip"`
|
||||||
|
|
||||||
LinuxDoConnectEnabled bool `json:"linuxdo_connect_enabled"`
|
LinuxDoConnectEnabled bool `json:"linuxdo_connect_enabled"`
|
||||||
LinuxDoConnectClientID string `json:"linuxdo_connect_client_id"`
|
LinuxDoConnectClientID string `json:"linuxdo_connect_client_id"`
|
||||||
|
|||||||
@ -757,6 +757,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"site_logo": "",
|
"site_logo": "",
|
||||||
"site_subtitle": "Subtitle",
|
"site_subtitle": "Subtitle",
|
||||||
"api_base_url": "https://api.example.com",
|
"api_base_url": "https://api.example.com",
|
||||||
|
"api_key_acl_trust_forwarded_ip": false,
|
||||||
"contact_info": "support",
|
"contact_info": "support",
|
||||||
"doc_url": "https://docs.example.com",
|
"doc_url": "https://docs.example.com",
|
||||||
"auth_source_default_email_balance": 0,
|
"auth_source_default_email_balance": 0,
|
||||||
@ -1014,6 +1015,7 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
"site_logo": "",
|
"site_logo": "",
|
||||||
"site_subtitle": "Subscription to API Conversion Platform",
|
"site_subtitle": "Subscription to API Conversion Platform",
|
||||||
"api_base_url": "",
|
"api_base_url": "",
|
||||||
|
"api_key_acl_trust_forwarded_ip": false,
|
||||||
"contact_info": "",
|
"contact_info": "",
|
||||||
"doc_url": "",
|
"doc_url": "",
|
||||||
"home_content": "",
|
"home_content": "",
|
||||||
|
|||||||
@ -90,6 +90,9 @@ func apiKeyAuthWithSubscription(apiKeyService *service.APIKeyService, subscripti
|
|||||||
// 注意:错误信息故意模糊,避免暴露具体的 IP 限制机制
|
// 注意:错误信息故意模糊,避免暴露具体的 IP 限制机制
|
||||||
if len(apiKey.IPWhitelist) > 0 || len(apiKey.IPBlacklist) > 0 {
|
if len(apiKey.IPWhitelist) > 0 || len(apiKey.IPBlacklist) > 0 {
|
||||||
clientIP := ip.GetTrustedClientIP(c)
|
clientIP := ip.GetTrustedClientIP(c)
|
||||||
|
if cfg.TrustForwardedIPForAPIKeyACL() {
|
||||||
|
clientIP = ip.GetClientIP(c)
|
||||||
|
}
|
||||||
allowed, _ := ip.CheckIPRestrictionWithCompiledRules(clientIP, apiKey.CompiledIPWhitelist, apiKey.CompiledIPBlacklist)
|
allowed, _ := ip.CheckIPRestrictionWithCompiledRules(clientIP, apiKey.CompiledIPWhitelist, apiKey.CompiledIPBlacklist)
|
||||||
if !allowed {
|
if !allowed {
|
||||||
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonIPRestriction)
|
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonIPRestriction)
|
||||||
|
|||||||
@ -398,7 +398,7 @@ func TestAPIKeyAuthRejectsUnavailableGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIKeyAuthIPRestrictionDoesNotTrustSpoofedForwardHeaders(t *testing.T) {
|
func TestAPIKeyAuthIPRestrictionDoesNotTrustForwardedClientIPByDefault(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
user := &service.User{
|
user := &service.User{
|
||||||
@ -460,6 +460,57 @@ func TestAPIKeyAuthIPRestrictionDoesNotTrustSpoofedForwardHeaders(t *testing.T)
|
|||||||
require.Equal(t, service.OpsClientBusinessLimitedReasonIPRestriction, businessLimitedReason)
|
require.Equal(t, service.OpsClientBusinessLimitedReasonIPRestriction, businessLimitedReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyAuthIPRestrictionCanTrustForwardedClientIPForReverseProxy(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
user := &service.User{
|
||||||
|
ID: 7,
|
||||||
|
Role: service.RoleUser,
|
||||||
|
Status: service.StatusActive,
|
||||||
|
Balance: 10,
|
||||||
|
Concurrency: 3,
|
||||||
|
}
|
||||||
|
apiKey := &service.APIKey{
|
||||||
|
ID: 100,
|
||||||
|
UserID: user.ID,
|
||||||
|
Key: "test-key",
|
||||||
|
Status: service.StatusActive,
|
||||||
|
User: user,
|
||||||
|
IPWhitelist: []string{"1.2.3.4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeyRepo := &stubApiKeyRepo{
|
||||||
|
getByKey: func(ctx context.Context, key string) (*service.APIKey, error) {
|
||||||
|
if key != apiKey.Key {
|
||||||
|
return nil, service.ErrAPIKeyNotFound
|
||||||
|
}
|
||||||
|
clone := *apiKey
|
||||||
|
return &clone, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &config.Config{RunMode: config.RunModeSimple}
|
||||||
|
cfg.SetTrustForwardedIPForAPIKeyACL(true)
|
||||||
|
apiKeyService := service.NewAPIKeyService(apiKeyRepo, nil, nil, nil, nil, nil, cfg)
|
||||||
|
router := gin.New()
|
||||||
|
require.NoError(t, router.SetTrustedProxies(nil))
|
||||||
|
router.Use(gin.HandlerFunc(NewAPIKeyAuthMiddleware(apiKeyService, nil, cfg)))
|
||||||
|
router.GET("/t", func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/t", nil)
|
||||||
|
req.RemoteAddr = "9.9.9.9:12345"
|
||||||
|
req.Header.Set("x-api-key", apiKey.Key)
|
||||||
|
req.Header.Set("X-Forwarded-For", "1.2.3.4")
|
||||||
|
req.Header.Set("X-Real-IP", "1.2.3.4")
|
||||||
|
req.Header.Set("CF-Connecting-IP", "1.2.3.4")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIKeyAuthTouchesLastUsedOnSuccess(t *testing.T) {
|
func TestAPIKeyAuthTouchesLastUsedOnSuccess(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/ip"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -31,7 +32,7 @@ func Logger() gin.HandlerFunc {
|
|||||||
|
|
||||||
method := c.Request.Method
|
method := c.Request.Method
|
||||||
statusCode := c.Writer.Status()
|
statusCode := c.Writer.Status()
|
||||||
clientIP := c.ClientIP()
|
clientIP := ip.GetClientIP(c)
|
||||||
protocol := c.Request.Proto
|
protocol := c.Request.Proto
|
||||||
accountID, hasAccountID := c.Request.Context().Value(ctxkey.AccountID).(int64)
|
accountID, hasAccountID := c.Request.Context().Value(ctxkey.AccountID).(int64)
|
||||||
platform, _ := c.Request.Context().Value(ctxkey.Platform).(string)
|
platform, _ := c.Request.Context().Value(ctxkey.Platform).(string)
|
||||||
|
|||||||
@ -180,6 +180,37 @@ func TestLogger_AccessLogIncludesCoreFields(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogger_AccessLogUsesForwardedClientIP(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
sink := initMiddlewareTestLogger(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(Logger())
|
||||||
|
r.GET("/api/test", func(c *gin.Context) {
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
|
||||||
|
req.RemoteAddr = "104.23.251.120:443"
|
||||||
|
req.Header.Set("CF-Connecting-IP", "203.0.113.42")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range sink.list() {
|
||||||
|
if event == nil || event.Message != "http request completed" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got := event.Fields["client_ip"]; got != "203.0.113.42" {
|
||||||
|
t.Fatalf("client_ip=%q, want real forwarded ip", got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("access log event not found")
|
||||||
|
}
|
||||||
|
|
||||||
func TestLogger_HealthPathSkipped(t *testing.T) {
|
func TestLogger_HealthPathSkipped(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
sink := initMiddlewareTestLogger(t)
|
sink := initMiddlewareTestLogger(t)
|
||||||
|
|||||||
@ -131,6 +131,9 @@ const (
|
|||||||
SettingKeyTurnstileSiteKey = "turnstile_site_key" // Turnstile Site Key
|
SettingKeyTurnstileSiteKey = "turnstile_site_key" // Turnstile Site Key
|
||||||
SettingKeyTurnstileSecretKey = "turnstile_secret_key" // Turnstile Secret Key
|
SettingKeyTurnstileSecretKey = "turnstile_secret_key" // Turnstile Secret Key
|
||||||
|
|
||||||
|
// API Key IP 访问控制设置
|
||||||
|
SettingKeyAPIKeyACLTrustForwardedIP = "api_key_acl_trust_forwarded_ip" // API Key IP 白/黑名单是否信任转发 IP
|
||||||
|
|
||||||
// TOTP 双因素认证设置
|
// TOTP 双因素认证设置
|
||||||
SettingKeyTotpEnabled = "totp_enabled" // 是否启用 TOTP 2FA 功能
|
SettingKeyTotpEnabled = "totp_enabled" // 是否启用 TOTP 2FA 功能
|
||||||
|
|
||||||
|
|||||||
@ -597,6 +597,23 @@ func (s *SettingService) SetProxyRepository(repo ProxyRepository) {
|
|||||||
s.proxyRepo = repo
|
s.proxyRepo = repo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) LoadAPIKeyACLTrustForwardedIPSetting(ctx context.Context) error {
|
||||||
|
if s == nil || s.cfg == nil || s.settingRepo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
value, err := s.settingRepo.GetValue(ctx, SettingKeyAPIKeyACLTrustForwardedIP)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrSettingNotFound) {
|
||||||
|
s.cfg.SetTrustForwardedIPForAPIKeyACL(s.cfg.Security.TrustForwardedIPForAPIKeyACL)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("get api key acl forwarded ip setting: %w", err)
|
||||||
|
}
|
||||||
|
enabled := value == "true"
|
||||||
|
s.cfg.SetTrustForwardedIPForAPIKeyACL(enabled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllSettings 获取所有系统设置
|
// GetAllSettings 获取所有系统设置
|
||||||
func (s *SettingService) GetAllSettings(ctx context.Context) (*SystemSettings, error) {
|
func (s *SettingService) GetAllSettings(ctx context.Context) (*SystemSettings, error) {
|
||||||
settings, err := s.settingRepo.GetAll(ctx)
|
settings, err := s.settingRepo.GetAll(ctx)
|
||||||
@ -633,6 +650,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
|
|||||||
SettingKeyLoginAgreementDocuments,
|
SettingKeyLoginAgreementDocuments,
|
||||||
SettingKeyTurnstileEnabled,
|
SettingKeyTurnstileEnabled,
|
||||||
SettingKeyTurnstileSiteKey,
|
SettingKeyTurnstileSiteKey,
|
||||||
|
SettingKeyAPIKeyACLTrustForwardedIP,
|
||||||
SettingKeySiteName,
|
SettingKeySiteName,
|
||||||
SettingKeySiteLogo,
|
SettingKeySiteLogo,
|
||||||
SettingKeySiteSubtitle,
|
SettingKeySiteSubtitle,
|
||||||
@ -1568,6 +1586,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
|
|||||||
if settings.TurnstileSecretKey != "" {
|
if settings.TurnstileSecretKey != "" {
|
||||||
updates[SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey
|
updates[SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey
|
||||||
}
|
}
|
||||||
|
updates[SettingKeyAPIKeyACLTrustForwardedIP] = strconv.FormatBool(settings.APIKeyACLTrustForwardedIP)
|
||||||
|
|
||||||
// LinuxDo Connect OAuth 登录
|
// LinuxDo Connect OAuth 登录
|
||||||
updates[SettingKeyLinuxDoConnectEnabled] = strconv.FormatBool(settings.LinuxDoConnectEnabled)
|
updates[SettingKeyLinuxDoConnectEnabled] = strconv.FormatBool(settings.LinuxDoConnectEnabled)
|
||||||
@ -1867,6 +1886,9 @@ func (s *SettingService) refreshCachedSettings(settings *SystemSettings) {
|
|||||||
enabled: settings.OpenAIAdvancedSchedulerEnabled,
|
enabled: settings.OpenAIAdvancedSchedulerEnabled,
|
||||||
expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
|
expiresAt: time.Now().Add(openAIAdvancedSchedulerSettingCacheTTL).UnixNano(),
|
||||||
})
|
})
|
||||||
|
if s.cfg != nil {
|
||||||
|
s.cfg.SetTrustForwardedIPForAPIKeyACL(settings.APIKeyACLTrustForwardedIP)
|
||||||
|
}
|
||||||
if s.onUpdate != nil {
|
if s.onUpdate != nil {
|
||||||
s.onUpdate() // Invalidate cache after settings update
|
s.onUpdate() // Invalidate cache after settings update
|
||||||
}
|
}
|
||||||
@ -2463,6 +2485,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|||||||
SettingKeyLoginAgreementMode: defaultLoginAgreementMode,
|
SettingKeyLoginAgreementMode: defaultLoginAgreementMode,
|
||||||
SettingKeyLoginAgreementUpdatedAt: defaultLoginAgreementDate,
|
SettingKeyLoginAgreementUpdatedAt: defaultLoginAgreementDate,
|
||||||
SettingKeyLoginAgreementDocuments: loginAgreementDocumentsJSON,
|
SettingKeyLoginAgreementDocuments: loginAgreementDocumentsJSON,
|
||||||
|
SettingKeyAPIKeyACLTrustForwardedIP: "false",
|
||||||
SettingKeySiteName: "Sub2API",
|
SettingKeySiteName: "Sub2API",
|
||||||
SettingKeySiteLogo: "",
|
SettingKeySiteLogo: "",
|
||||||
SettingKeyPurchaseSubscriptionEnabled: "false",
|
SettingKeyPurchaseSubscriptionEnabled: "false",
|
||||||
@ -2622,6 +2645,12 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
if loginAgreementUpdatedAt == "" {
|
if loginAgreementUpdatedAt == "" {
|
||||||
loginAgreementUpdatedAt = defaultLoginAgreementDate
|
loginAgreementUpdatedAt = defaultLoginAgreementDate
|
||||||
}
|
}
|
||||||
|
apiKeyACLTrustForwardedIP := false
|
||||||
|
if value, ok := settings[SettingKeyAPIKeyACLTrustForwardedIP]; ok {
|
||||||
|
apiKeyACLTrustForwardedIP = value == "true"
|
||||||
|
} else if s != nil && s.cfg != nil {
|
||||||
|
apiKeyACLTrustForwardedIP = s.cfg.Security.TrustForwardedIPForAPIKeyACL
|
||||||
|
}
|
||||||
result := &SystemSettings{
|
result := &SystemSettings{
|
||||||
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
|
||||||
EmailVerifyEnabled: emailVerifyEnabled,
|
EmailVerifyEnabled: emailVerifyEnabled,
|
||||||
@ -2644,6 +2673,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|||||||
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
||||||
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
||||||
TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "",
|
TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "",
|
||||||
|
APIKeyACLTrustForwardedIP: apiKeyACLTrustForwardedIP,
|
||||||
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
SiteName: s.getStringOrDefault(settings, SettingKeySiteName, "Sub2API"),
|
||||||
SiteLogo: settings[SettingKeySiteLogo],
|
SiteLogo: settings[SettingKeySiteLogo],
|
||||||
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
SiteSubtitle: s.getStringOrDefault(settings, SettingKeySiteSubtitle, "Subscription to API Conversion Platform"),
|
||||||
|
|||||||
@ -290,6 +290,30 @@ func TestSettingService_UpdateSettings_AntigravityUserAgentVersion(t *testing.T)
|
|||||||
require.Equal(t, "1.23.2", repo.updates[SettingKeyAntigravityUserAgentVersion])
|
require.Equal(t, "1.23.2", repo.updates[SettingKeyAntigravityUserAgentVersion])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSettingService_UpdateSettings_APIKeyACLTrustForwardedIPRefreshesConfig(t *testing.T) {
|
||||||
|
repo := &settingUpdateRepoStub{}
|
||||||
|
cfg := &config.Config{}
|
||||||
|
svc := NewSettingService(repo, cfg)
|
||||||
|
|
||||||
|
err := svc.UpdateSettings(context.Background(), &SystemSettings{
|
||||||
|
APIKeyACLTrustForwardedIP: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "true", repo.updates[SettingKeyAPIKeyACLTrustForwardedIP])
|
||||||
|
require.True(t, cfg.Security.TrustForwardedIPForAPIKeyACL)
|
||||||
|
require.True(t, cfg.TrustForwardedIPForAPIKeyACL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSettingService_ParseSettings_APIKeyACLTrustForwardedIPFallsBackToConfigWhenMissing(t *testing.T) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
cfg.Security.TrustForwardedIPForAPIKeyACL = true
|
||||||
|
svc := NewSettingService(&settingUpdateRepoStub{}, cfg)
|
||||||
|
|
||||||
|
got := svc.parseSettings(map[string]string{})
|
||||||
|
|
||||||
|
require.True(t, got.APIKeyACLTrustForwardedIP)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSettingService_GetAntigravityUserAgentVersion_Precedence(t *testing.T) {
|
func TestSettingService_GetAntigravityUserAgentVersion_Precedence(t *testing.T) {
|
||||||
t.Run("后台设置优先", func(t *testing.T) {
|
t.Run("后台设置优先", func(t *testing.T) {
|
||||||
svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
|
svc := NewSettingService(&settingAntigravityUARepoStub{values: map[string]string{
|
||||||
|
|||||||
@ -38,6 +38,7 @@ type SystemSettings struct {
|
|||||||
TurnstileSiteKey string
|
TurnstileSiteKey string
|
||||||
TurnstileSecretKey string
|
TurnstileSecretKey string
|
||||||
TurnstileSecretKeyConfigured bool
|
TurnstileSecretKeyConfigured bool
|
||||||
|
APIKeyACLTrustForwardedIP bool
|
||||||
|
|
||||||
// LinuxDo Connect OAuth 登录
|
// LinuxDo Connect OAuth 登录
|
||||||
LinuxDoConnectEnabled bool
|
LinuxDoConnectEnabled bool
|
||||||
|
|||||||
@ -397,6 +397,9 @@ func ProvideSettingService(settingRepo SettingRepository, groupRepo GroupReposit
|
|||||||
svc := NewSettingService(settingRepo, cfg)
|
svc := NewSettingService(settingRepo, cfg)
|
||||||
svc.SetDefaultSubscriptionGroupReader(groupRepo)
|
svc.SetDefaultSubscriptionGroupReader(groupRepo)
|
||||||
svc.SetProxyRepository(proxyRepo)
|
svc.SetProxyRepository(proxyRepo)
|
||||||
|
if err := svc.LoadAPIKeyACLTrustForwardedIPSetting(context.Background()); err != nil {
|
||||||
|
logger.LegacyPrintf("service.setting", "Warning: load api key acl forwarded ip setting failed: %v", err)
|
||||||
|
}
|
||||||
antigravity.SetUserAgentVersionResolver(svc.GetAntigravityUserAgentVersion)
|
antigravity.SetUserAgentVersionResolver(svc.GetAntigravityUserAgentVersion)
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|||||||
@ -396,6 +396,7 @@ export interface SystemSettings {
|
|||||||
turnstile_enabled: boolean;
|
turnstile_enabled: boolean;
|
||||||
turnstile_site_key: string;
|
turnstile_site_key: string;
|
||||||
turnstile_secret_key_configured: boolean;
|
turnstile_secret_key_configured: boolean;
|
||||||
|
api_key_acl_trust_forwarded_ip: boolean;
|
||||||
|
|
||||||
// LinuxDo Connect OAuth settings
|
// LinuxDo Connect OAuth settings
|
||||||
linuxdo_connect_enabled: boolean;
|
linuxdo_connect_enabled: boolean;
|
||||||
@ -637,6 +638,7 @@ export interface UpdateSettingsRequest {
|
|||||||
turnstile_enabled?: boolean;
|
turnstile_enabled?: boolean;
|
||||||
turnstile_site_key?: string;
|
turnstile_site_key?: string;
|
||||||
turnstile_secret_key?: string;
|
turnstile_secret_key?: string;
|
||||||
|
api_key_acl_trust_forwarded_ip?: boolean;
|
||||||
linuxdo_connect_enabled?: boolean;
|
linuxdo_connect_enabled?: boolean;
|
||||||
linuxdo_connect_client_id?: string;
|
linuxdo_connect_client_id?: string;
|
||||||
linuxdo_connect_client_secret?: string;
|
linuxdo_connect_client_secret?: string;
|
||||||
|
|||||||
@ -5309,7 +5309,15 @@ export default {
|
|||||||
siteKeyHint: 'Get this from your Cloudflare Dashboard',
|
siteKeyHint: 'Get this from your Cloudflare Dashboard',
|
||||||
cloudflareDashboard: 'Cloudflare Dashboard',
|
cloudflareDashboard: 'Cloudflare Dashboard',
|
||||||
secretKeyHint: 'Server-side verification key (keep this secret)',
|
secretKeyHint: 'Server-side verification key (keep this secret)',
|
||||||
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' },
|
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.'
|
||||||
|
},
|
||||||
|
apiKeyAcl: {
|
||||||
|
title: 'API Key IP Access Control',
|
||||||
|
description: 'Choose which client IP is used by API Key allowlists and denylists',
|
||||||
|
trustForwardedIp: 'Trust forwarded client IP',
|
||||||
|
trustForwardedIpHint:
|
||||||
|
'Disabled by default. Enable only when the origin is reachable only through Cloudflare or Nginx reverse proxy. When enabled, API Key IP allowlists and denylists use CF-Connecting-IP, X-Real-IP, or X-Forwarded-For, matching the request IP shown in usage records.'
|
||||||
|
},
|
||||||
linuxdo: {
|
linuxdo: {
|
||||||
title: 'LinuxDo Connect Login',
|
title: 'LinuxDo Connect Login',
|
||||||
description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login',
|
description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login',
|
||||||
|
|||||||
@ -5474,6 +5474,13 @@ export default {
|
|||||||
secretKeyHint: '服务端验证密钥(请保密)',
|
secretKeyHint: '服务端验证密钥(请保密)',
|
||||||
secretKeyConfiguredHint: '密钥已配置,留空以保留当前值。'
|
secretKeyConfiguredHint: '密钥已配置,留空以保留当前值。'
|
||||||
},
|
},
|
||||||
|
apiKeyAcl: {
|
||||||
|
title: 'API Key IP 访问控制',
|
||||||
|
description: '控制 API Key 白名单和黑名单使用哪个客户端 IP 判断',
|
||||||
|
trustForwardedIp: '信任反代传递的客户端 IP',
|
||||||
|
trustForwardedIpHint:
|
||||||
|
'默认关闭。仅在源站只允许 Cloudflare 或 Nginx 反代访问时开启;开启后 API Key IP 白/黑名单会使用 CF-Connecting-IP、X-Real-IP 或 X-Forwarded-For,与使用记录中的请求 IP 保持一致。'
|
||||||
|
},
|
||||||
linuxdo: {
|
linuxdo: {
|
||||||
title: 'LinuxDo Connect 登录',
|
title: 'LinuxDo Connect 登录',
|
||||||
description: '配置 LinuxDo Connect OAuth,用于 Sub2API 用户登录',
|
description: '配置 LinuxDo Connect OAuth,用于 Sub2API 用户登录',
|
||||||
|
|||||||
@ -1564,6 +1564,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key IP ACL Settings -->
|
||||||
|
<div class="card">
|
||||||
|
<div
|
||||||
|
class="border-b border-gray-100 px-6 py-4 dark:border-dark-700"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ t("admin.settings.apiKeyAcl.title") }}
|
||||||
|
</h2>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t("admin.settings.apiKeyAcl.description") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5 p-6">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ t("admin.settings.apiKeyAcl.trustForwardedIp") }}
|
||||||
|
</label>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t("admin.settings.apiKeyAcl.trustForwardedIpHint") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle v-model="form.api_key_acl_trust_forwarded_ip" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Cloudflare Turnstile Settings -->
|
<!-- Cloudflare Turnstile Settings -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div
|
<div
|
||||||
@ -6868,6 +6895,7 @@ const form = reactive<SettingsForm>({
|
|||||||
turnstile_site_key: "",
|
turnstile_site_key: "",
|
||||||
turnstile_secret_key: "",
|
turnstile_secret_key: "",
|
||||||
turnstile_secret_key_configured: false,
|
turnstile_secret_key_configured: false,
|
||||||
|
api_key_acl_trust_forwarded_ip: false,
|
||||||
// LinuxDo Connect OAuth 登录
|
// LinuxDo Connect OAuth 登录
|
||||||
linuxdo_connect_enabled: false,
|
linuxdo_connect_enabled: false,
|
||||||
linuxdo_connect_client_id: "",
|
linuxdo_connect_client_id: "",
|
||||||
@ -7972,6 +8000,7 @@ async function saveSettings() {
|
|||||||
turnstile_enabled: form.turnstile_enabled,
|
turnstile_enabled: form.turnstile_enabled,
|
||||||
turnstile_site_key: form.turnstile_site_key,
|
turnstile_site_key: form.turnstile_site_key,
|
||||||
turnstile_secret_key: form.turnstile_secret_key || undefined,
|
turnstile_secret_key: form.turnstile_secret_key || undefined,
|
||||||
|
api_key_acl_trust_forwarded_ip: form.api_key_acl_trust_forwarded_ip,
|
||||||
linuxdo_connect_enabled: form.linuxdo_connect_enabled,
|
linuxdo_connect_enabled: form.linuxdo_connect_enabled,
|
||||||
linuxdo_connect_client_id: form.linuxdo_connect_client_id,
|
linuxdo_connect_client_id: form.linuxdo_connect_client_id,
|
||||||
linuxdo_connect_client_secret:
|
linuxdo_connect_client_secret:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user