mirror of
https://github.com/nikdoof/pocket-id.git
synced 2025-12-14 07:12:19 +00:00
fix: ensure the downloaded GeoLite2 DB is not corrupted & prevent RW race condition (#138)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user