fix: ensure the downloaded GeoLite2 DB is not corrupted & prevent RW race condition (#138)

This commit is contained in:
Giovanni
2025-01-20 18:50:58 +08:00
committed by GitHub
parent 72923bb86d
commit f7710f2988
5 changed files with 53 additions and 18 deletions

View File

@@ -21,7 +21,7 @@ RUN CGO_ENABLED=1 GOOS=linux go build -o /app/backend/pocket-id-backend .
# Stage 3: Production Image # Stage 3: Production Image
FROM node:20-alpine FROM node:20-alpine
# Delete default node user # Delete default node user
RUN deluser --remove-home node RUN deluser --remove-home node
RUN apk add --no-cache caddy curl su-exec RUN apk add --no-cache caddy curl su-exec

View File

@@ -37,6 +37,6 @@ type AppConfigUpdateDto struct {
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"` LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
LdapAttributeGroupName string `json:"ldapAttributeGroupName"` LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"` LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"` EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"` EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
} }

View File

@@ -20,14 +20,14 @@ type AppConfig struct {
LogoLightImageType AppConfigVariable LogoLightImageType AppConfigVariable
LogoDarkImageType AppConfigVariable LogoDarkImageType AppConfigVariable
// Email // Email
SmtpHost AppConfigVariable SmtpHost AppConfigVariable
SmtpPort AppConfigVariable SmtpPort AppConfigVariable
SmtpFrom AppConfigVariable SmtpFrom AppConfigVariable
SmtpUser AppConfigVariable SmtpUser AppConfigVariable
SmtpPassword AppConfigVariable SmtpPassword AppConfigVariable
SmtpTls AppConfigVariable SmtpTls AppConfigVariable
SmtpSkipCertVerify AppConfigVariable SmtpSkipCertVerify AppConfigVariable
EmailLoginNotificationEnabled AppConfigVariable EmailLoginNotificationEnabled AppConfigVariable
EmailOneTimeAccessEnabled AppConfigVariable EmailOneTimeAccessEnabled AppConfigVariable
// LDAP // LDAP
LdapEnabled AppConfigVariable LdapEnabled AppConfigVariable

View File

@@ -73,7 +73,7 @@ var defaultDbConfig = model.AppConfig{
IsInternal: true, IsInternal: true,
DefaultValue: "svg", DefaultValue: "svg",
}, },
// Email // Email
SmtpHost: model.AppConfigVariable{ SmtpHost: model.AppConfigVariable{
Key: "smtpHost", Key: "smtpHost",
Type: "string", Type: "string",
@@ -104,7 +104,7 @@ var defaultDbConfig = model.AppConfig{
Type: "bool", Type: "bool",
DefaultValue: "false", DefaultValue: "false",
}, },
EmailLoginNotificationEnabled: model.AppConfigVariable{ EmailLoginNotificationEnabled: model.AppConfigVariable{
Key: "emailLoginNotificationEnabled", Key: "emailLoginNotificationEnabled",
Type: "bool", Type: "bool",
DefaultValue: "false", DefaultValue: "false",

View File

@@ -12,6 +12,7 @@ import (
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"github.com/oschwald/maxminddb-golang/v2" "github.com/oschwald/maxminddb-golang/v2"
@@ -19,7 +20,9 @@ import (
"github.com/stonith404/pocket-id/backend/internal/common" "github.com/stonith404/pocket-id/backend/internal/common"
) )
type GeoLiteService struct{} type GeoLiteService struct {
mutex sync.Mutex
}
var localhostIPNets = []*net.IPNet{ var localhostIPNets = []*net.IPNet{
{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8 {IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
@@ -70,6 +73,10 @@ func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string
} }
} }
// Race condition between reading and writing the database.
s.mutex.Lock()
defer s.mutex.Unlock()
db, err := maxminddb.Open(common.EnvConfig.GeoLiteDBPath) db, err := maxminddb.Open(common.EnvConfig.GeoLiteDBPath)
if err != nil { if err != nil {
return "", "", err return "", "", err
@@ -161,16 +168,44 @@ func (s *GeoLiteService) extractDatabase(reader io.Reader) error {
// Check if the file is the GeoLite2-City.mmdb file // Check if the file is the GeoLite2-City.mmdb file
if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "GeoLite2-City.mmdb" { if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "GeoLite2-City.mmdb" {
outFile, err := os.Create(common.EnvConfig.GeoLiteDBPath) // extract to a temporary file to avoid having a corrupted db in case of write failure.
baseDir := filepath.Dir(common.EnvConfig.GeoLiteDBPath)
tmpFile, err := os.CreateTemp(baseDir, "geolite.*.mmdb.tmp")
if err != nil { if err != nil {
return fmt.Errorf("failed to create target database file: %w", err) return fmt.Errorf("failed to create temporary database file: %w", err)
} }
defer outFile.Close() tempName := tmpFile.Name()
// Write the file contents directly to the target location // Write the file contents directly to the target location
if _, err := io.Copy(outFile, tarReader); err != nil { if _, err := io.Copy(tmpFile, tarReader); err != nil {
// if fails to write, then cleanup and throw an error
tmpFile.Close()
os.Remove(tempName)
return fmt.Errorf("failed to write database file: %w", err) return fmt.Errorf("failed to write database file: %w", err)
} }
tmpFile.Close()
// ensure the database is not corrupted
db, err := maxminddb.Open(tempName)
if err != nil {
// if fails to write, then cleanup and throw an error
os.Remove(tempName)
return fmt.Errorf("failed to open downloaded database file: %w", err)
}
db.Close()
// ensure we lock the structure before we overwrite the database
// to prevent race conditions between reading and writing the mmdb.
s.mutex.Lock()
// replace the old file with the new file
err = os.Rename(tempName, common.EnvConfig.GeoLiteDBPath)
s.mutex.Unlock()
if err != nil {
// if cannot overwrite via rename, then cleanup and throw an error
os.Remove(tempName)
return fmt.Errorf("failed to replace database file: %w", err)
}
return nil return nil
} }
} }