mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-29 12:09:05 +01:00
feat: add support Cloudflare location headers
This commit is contained in:
@@ -103,8 +103,8 @@ func Bootstrap(ctx context.Context) error {
|
||||
// Migrate the pre-actor signup tokens into their actors, once the actor host is ready
|
||||
services = append(services, actorsReady.Await(svc.userSignUpModule.RunSignupTokenMigration))
|
||||
|
||||
// These services are only registered in non-test mode
|
||||
if common.EnvConfig.AppEnv != "test" {
|
||||
// Only the GeoLite provider needs a background database refresher
|
||||
if !common.EnvConfig.AppEnv.IsTest() && svc.geoLiteModule != nil {
|
||||
// Refresh the GeoLite database (this is cached per each replica)
|
||||
services = append(services, svc.geoLiteModule.Run)
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ func shouldTraceRequest(r *http.Request) bool {
|
||||
}
|
||||
|
||||
func registerGlobalMiddleware(r *gin.Engine) {
|
||||
if common.EnvConfig.CloudflareLocationHeaders {
|
||||
r.Use(middleware.CloudflareLocationMiddleware())
|
||||
}
|
||||
r.Use(middleware.HeadMiddleware())
|
||||
r.Use(middleware.NewCacheControlMiddleware().Add())
|
||||
r.Use(middleware.NewCorsMiddleware().Add())
|
||||
|
||||
@@ -21,10 +21,70 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/apperror"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/geolite"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/iplocation"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCloudflareLocationMiddlewareOnlyWhenEnabled(t *testing.T) {
|
||||
previousConfig := common.EnvConfig
|
||||
t.Cleanup(func() { common.EnvConfig = previousConfig })
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
for _, enabled := range []bool{false, true} {
|
||||
name := "disabled"
|
||||
if enabled {
|
||||
name = "enabled"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
common.EnvConfig.CloudflareLocationHeaders = enabled
|
||||
locator, geoLiteModule, err := initIPLocationResolver(t.Context(), nil, &common.EnvConfigSchema{
|
||||
GeoLiteDBPath: filepath.Join(t.TempDir(), "missing.mmdb"),
|
||||
CloudflareLocationHeaders: enabled,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
if enabled {
|
||||
require.IsType(t, iplocation.NewCloudflareResolver(), locator)
|
||||
require.Nil(t, geoLiteModule)
|
||||
} else {
|
||||
require.IsType(t, &geolite.Module{}, locator)
|
||||
require.Same(t, geoLiteModule, locator)
|
||||
}
|
||||
|
||||
// Exercise the real global middleware with a Cloudflare client IP instead of the proxy's IP
|
||||
router := gin.New()
|
||||
router.TrustedPlatform = "CF-Connecting-IP"
|
||||
registerGlobalMiddleware(router)
|
||||
router.GET("/api/test", func(c *gin.Context) {
|
||||
country, city, err := locator.GetLocationByIP(c.Request.Context(), c.ClientIP())
|
||||
require.NoError(t, err)
|
||||
c.JSON(http.StatusOK, gin.H{"ip": c.ClientIP(), "country": country, "city": city})
|
||||
})
|
||||
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/test", nil)
|
||||
request.RemoteAddr = "192.168.1.1:1234"
|
||||
request.Header.Set("cf-connecting-ip", "81.2.69.142")
|
||||
request.Header.Set("cf-ipcountry", "CH")
|
||||
request.Header.Set("cf-ipcity", "Zürich")
|
||||
recorder := httptest.NewRecorder()
|
||||
router.ServeHTTP(recorder, request)
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var location map[string]string
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &location))
|
||||
require.Equal(t, "81.2.69.142", location["ip"])
|
||||
if enabled {
|
||||
require.Equal(t, "Switzerland", location["country"])
|
||||
require.Equal(t, "Zürich", location["city"])
|
||||
} else {
|
||||
require.Empty(t, location["country"])
|
||||
require.Empty(t, location["city"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestLoggerUsesStructuredErrorMetadata(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/emailverification"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/environment"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/geolite"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/iplocation"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/ldapsync"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/oidc"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/onetimeaccess"
|
||||
@@ -33,6 +34,7 @@ type services struct {
|
||||
appImagesService *service.AppImagesService
|
||||
emailModule *email.Module
|
||||
geoLiteModule *geolite.Module
|
||||
ipLocator iplocation.Resolver
|
||||
auditLogService *service.AuditLogService
|
||||
jwtService *service.JwtService
|
||||
userService *service.UserService
|
||||
@@ -84,17 +86,13 @@ func initServices(
|
||||
return nil, fmt.Errorf("failed to create email module: %w", err)
|
||||
}
|
||||
|
||||
svc.geoLiteModule, err = geolite.New(ctx, geolite.Dependencies{
|
||||
HTTPClient: httpClient,
|
||||
DBPath: common.EnvConfig.GeoLiteDBPath,
|
||||
DownloadURL: common.EnvConfig.GeoLiteDBUrl,
|
||||
LicenseKey: common.EnvConfig.MaxMindLicenseKey,
|
||||
})
|
||||
// Select the location provider once so all consumers use the configured implementation
|
||||
svc.ipLocator, svc.geoLiteModule, err = initIPLocationResolver(ctx, httpClient, &common.EnvConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GeoLite module: %w", err)
|
||||
return nil, fmt.Errorf("failed to create IP location resolver: %w", err)
|
||||
}
|
||||
|
||||
svc.auditLogService = service.NewAuditLogService(db, svc.emailModule, svc.geoLiteModule, svc.appConfigService)
|
||||
svc.auditLogService = service.NewAuditLogService(db, svc.emailModule, svc.ipLocator, svc.appConfigService)
|
||||
svc.auditLogsModule, err = auditlogs.New(auditlogs.Dependencies{
|
||||
DB: db,
|
||||
Actors: actors,
|
||||
@@ -132,7 +130,7 @@ func initServices(
|
||||
Signer: svc.jwtService,
|
||||
Reauth: svc.webauthnModule,
|
||||
AuditLog: svc.auditLogService,
|
||||
IPLocator: svc.geoLiteModule,
|
||||
IPLocator: svc.ipLocator,
|
||||
AppConfig: svc.appConfigService,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -262,3 +260,21 @@ func initServices(
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func initIPLocationResolver(ctx context.Context, httpClient *http.Client, config *common.EnvConfigSchema) (iplocation.Resolver, *geolite.Module, error) {
|
||||
// Cloudflare locations need no database initialization or background refresh
|
||||
if config.CloudflareLocationHeaders {
|
||||
return iplocation.NewCloudflareResolver(), nil, nil
|
||||
}
|
||||
|
||||
module, err := geolite.New(ctx, geolite.Dependencies{
|
||||
HTTPClient: httpClient,
|
||||
DBPath: config.GeoLiteDBPath,
|
||||
DownloadURL: config.GeoLiteDBUrl,
|
||||
LicenseKey: config.MaxMindLicenseKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return module, module, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user