diff --git a/go-backend/cmd/api/main.go b/go-backend/cmd/api/main.go index c31ce98..9250bad 100644 --- a/go-backend/cmd/api/main.go +++ b/go-backend/cmd/api/main.go @@ -89,6 +89,16 @@ func main() { MaxAge: 12 * time.Hour, })) + // Initialize GeoIP middleware for geographic blocking + geoIPMiddleware, err := middleware.NewGeoIPMiddleware("/opt/sojorn/geoip/GeoLite2-Country.mmdb") + if err != nil { + log.Warn().Err(err).Msg("Failed to initialize GeoIP middleware, geographic filtering disabled") + } else { + defer geoIPMiddleware.Close() + r.Use(geoIPMiddleware.Middleware()) + log.Info().Msg("GeoIP middleware enabled - blocking requests from outside North America") + } + r.NoRoute(func(c *gin.Context) { log.Debug().Msgf("No route found for %s %s", c.Request.Method, c.Request.URL.Path) c.JSON(404, gin.H{"error": "route not found", "path": c.Request.URL.Path, "method": c.Request.Method}) diff --git a/go-backend/go.mod b/go-backend/go.mod index 4f57e91..054c23a 100644 --- a/go-backend/go.mod +++ b/go-backend/go.mod @@ -87,6 +87,8 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oschwald/geoip2-golang v1.13.0 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/quic-go/qpack v0.5.1 // indirect diff --git a/go-backend/go.sum b/go-backend/go.sum index ac951b4..fdadb7c 100644 --- a/go-backend/go.sum +++ b/go-backend/go.sum @@ -180,6 +180,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= +github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= github.com/pashagolub/pgxmock/v4 v4.9.0 h1:itlO8nrVRnzkdMBXLs8pWUyyB2PC3Gku0WGIj/gGl7I= github.com/pashagolub/pgxmock/v4 v4.9.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= diff --git a/go-backend/internal/middleware/geoip.go b/go-backend/internal/middleware/geoip.go new file mode 100644 index 0000000..aabd0fa --- /dev/null +++ b/go-backend/internal/middleware/geoip.go @@ -0,0 +1,92 @@ +package middleware + +import ( + "net" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/oschwald/geoip2-golang" + "github.com/rs/zerolog/log" +) + +// GeoIPMiddleware blocks requests from outside North America +type GeoIPMiddleware struct { + db *geoip2.Reader +} + +// NewGeoIPMiddleware creates a new GeoIP middleware +func NewGeoIPMiddleware(dbPath string) (*GeoIPMiddleware, error) { + db, err := geoip2.Open(dbPath) + if err != nil { + return nil, err + } + + return &GeoIPMiddleware{db: db}, nil +} + +// Close closes the GeoIP database +func (g *GeoIPMiddleware) Close() error { + return g.db.Close() +} + +// Middleware returns the Gin middleware function +func (g *GeoIPMiddleware) Middleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Get client IP + clientIP := c.ClientIP() + + // Parse IP + ip := net.ParseIP(clientIP) + if ip == nil { + // Invalid IP, block it + log.Warn().Str("ip", clientIP).Msg("Invalid IP address, blocking request") + c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) + c.Abort() + return + } + + // Skip for private/local IPs + if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() { + c.Next() + return + } + + // Look up country + record, err := g.db.Country(ip) + if err != nil { + log.Warn().Str("ip", clientIP).Err(err).Msg("Failed to lookup country, blocking request") + c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}) + c.Abort() + return + } + + // Check if country is in North America + countryCode := record.Country.IsoCode + if !g.isNorthAmericanCountry(countryCode) { + log.Info().Str("ip", clientIP).Str("country", countryCode).Msg("Blocking request from outside North America") + c.JSON(http.StatusForbidden, gin.H{"error": "Access denied - region not supported"}) + c.Abort() + return + } + + c.Next() + } +} + +// isNorthAmericanCountry checks if the country code is from North America +func (g *GeoIPMiddleware) isNorthAmericanCountry(countryCode string) bool { + northAmericanCountries := map[string]bool{ + "US": true, // United States + "CA": true, // Canada + "MX": true, // Mexico + "GT": true, // Guatemala + "BZ": true, // Belize + "SV": true, // El Salvador + "HN": true, // Honduras + "NI": true, // Nicaragua + "CR": true, // Costa Rica + "PA": true, // Panama + } + + return northAmericanCountries[countryCode] +} diff --git a/go-backend/scripts/setup-geoip.sh b/go-backend/scripts/setup-geoip.sh new file mode 100644 index 0000000..2fe3036 --- /dev/null +++ b/go-backend/scripts/setup-geoip.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Setup script for GeoIP database +# This downloads the GeoLite2-Country database from MaxMind + +set -e + +GEOIP_DIR="/opt/sojorn/geoip" +DATABASE_FILE="$GEOIP_DIR/GeoLite2-Country.mmdb" + +echo "Setting up GeoIP database for geographic filtering..." + +# Create directory if it doesn't exist +sudo mkdir -p "$GEOIP_DIR" +sudo chown patrick:patrick "$GEOIP_DIR" + +# Download the GeoLite2-Country database +echo "Downloading GeoLite2-Country database..." +cd "$GEOIP_DIR" + +# Use curl to download the database (you'll need a MaxMind account for this) +# For now, we'll create a placeholder - you should replace this with actual download +echo "Note: You need to sign up for a free MaxMind account to download the GeoLite2 database" +echo "Visit: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data" +echo "" +echo "After getting your license key, download with:" +echo "curl -v 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz' | tar xz --strip-components=1 -C ." +echo "" + +# Create a placeholder file for now (this won't work but allows the service to start) +echo "Creating placeholder database file (you should replace this with the real database)" +cat > placeholder.txt << 'EOF' +This is a placeholder file. You need to download the actual GeoLite2-Country.mmdb file +from MaxMind and place it here: /opt/sojorn/geoip/GeoLite2-Country.mmdb + +Steps: +1. Sign up for free account at https://dev.maxmind.com/geoip/geolite2-free-geolocation-data +2. Get your license key +3. Download: curl -v 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz' | tar xz --strip-components=1 -C . +EOF + +echo "Setup complete. Please download the actual GeoIP database as described above." +echo "The service will start but geographic filtering will be disabled until the database is installed."