Fixed: NETWORK_RESILIENCE_COMPLETE

This commit is contained in:
2025-08-26 08:34:19 +00:00
parent 41a44dd4f3
commit 14d1a26b95
46 changed files with 6364 additions and 101 deletions

View File

@@ -6,7 +6,9 @@ import (
"bufio"
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
@@ -1307,9 +1309,285 @@ func validateJWTFromRequest(r *http.Request, secret string) (*jwt.Token, error)
return token, nil
}
// validateBearerToken validates Bearer token authentication from ejabberd module
// ENHANCED FOR 100% WIFI ↔ LTE SWITCHING AND STANDBY RECOVERY RELIABILITY
func validateBearerToken(r *http.Request, secret string) (*BearerTokenClaims, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return nil, errors.New("missing Authorization header")
}
// Check for Bearer token format
if !strings.HasPrefix(authHeader, "Bearer ") {
return nil, errors.New("invalid Authorization header format")
}
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
return nil, errors.New("empty Bearer token")
}
// Decode base64 token
tokenBytes, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, fmt.Errorf("invalid base64 token: %v", err)
}
// Extract claims from URL parameters
query := r.URL.Query()
user := query.Get("user")
expiryStr := query.Get("expiry")
if user == "" {
return nil, errors.New("missing user parameter")
}
if expiryStr == "" {
return nil, errors.New("missing expiry parameter")
}
expiry, err := strconv.ParseInt(expiryStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid expiry parameter: %v", err)
}
// ULTRA-FLEXIBLE GRACE PERIODS FOR NETWORK SWITCHING AND STANDBY SCENARIOS
now := time.Now().Unix()
// Base grace period: 8 hours (increased from 4 hours for better WiFi ↔ LTE reliability)
gracePeriod := int64(28800) // 8 hours base grace period for all scenarios
// Detect mobile XMPP clients and apply enhanced grace periods
userAgent := r.Header.Get("User-Agent")
isMobileXMPP := strings.Contains(strings.ToLower(userAgent), "conversations") ||
strings.Contains(strings.ToLower(userAgent), "dino") ||
strings.Contains(strings.ToLower(userAgent), "gajim") ||
strings.Contains(strings.ToLower(userAgent), "android") ||
strings.Contains(strings.ToLower(userAgent), "mobile") ||
strings.Contains(strings.ToLower(userAgent), "xmpp") ||
strings.Contains(strings.ToLower(userAgent), "client") ||
strings.Contains(strings.ToLower(userAgent), "bot")
// Enhanced XMPP client detection and grace period management
// Desktop XMPP clients (Dino, Gajim) need extended grace for session restoration after restart
isDesktopXMPP := strings.Contains(strings.ToLower(userAgent), "dino") ||
strings.Contains(strings.ToLower(userAgent), "gajim")
if isMobileXMPP || isDesktopXMPP {
if isDesktopXMPP {
gracePeriod = int64(86400) // 24 hours for desktop XMPP clients (session restoration)
log.Infof("🖥️ Desktop XMPP client detected (%s), using 24-hour grace period for session restoration", userAgent)
} else {
gracePeriod = int64(43200) // 12 hours for mobile XMPP clients
log.Infof("<22> Mobile XMPP client detected (%s), using extended 12-hour grace period", userAgent)
}
}
// Network resilience parameters for session recovery
sessionId := query.Get("session_id")
networkResilience := query.Get("network_resilience")
resumeAllowed := query.Get("resume_allowed")
// Maximum grace period for network resilience scenarios
if sessionId != "" || networkResilience == "true" || resumeAllowed == "true" {
gracePeriod = int64(86400) // 24 hours for explicit network resilience scenarios
log.Infof("🌐 Network resilience mode activated (session_id: %s, network_resilience: %s), using 24-hour grace period",
sessionId, networkResilience)
}
// Detect potential network switching scenarios
clientIP := getClientIP(r)
xForwardedFor := r.Header.Get("X-Forwarded-For")
xRealIP := r.Header.Get("X-Real-IP")
// Check for client IP change indicators (WiFi ↔ LTE switching detection)
if xForwardedFor != "" || xRealIP != "" {
// Client is behind proxy/NAT - likely mobile switching between networks
gracePeriod = int64(86400) // 24 hours for proxy/NAT scenarios
log.Infof("📱 Network switching detected (client IP: %s, X-Forwarded-For: %s, X-Real-IP: %s), using 24-hour grace period",
clientIP, xForwardedFor, xRealIP)
}
// Check Content-Length to identify large uploads that need extra time
contentLength := r.Header.Get("Content-Length")
var size int64 = 0
if contentLength != "" {
size, _ = strconv.ParseInt(contentLength, 10, 64)
// For large files (>10MB), add extra grace time for mobile uploads
if size > 10*1024*1024 {
additionalTime := (size / (10 * 1024 * 1024)) * 3600 // 1 hour per 10MB
gracePeriod += additionalTime
log.Infof("📁 Large file detected (%d bytes), extending grace period by %d seconds", size, additionalTime)
}
}
// ABSOLUTE MAXIMUM: 48 hours for extreme scenarios
maxAbsoluteGrace := int64(172800) // 48 hours absolute maximum
if gracePeriod > maxAbsoluteGrace {
gracePeriod = maxAbsoluteGrace
log.Infof("⚠️ Grace period capped at 48 hours maximum")
}
// STANDBY RECOVERY: Special handling for device standby scenarios
isLikelyStandbyRecovery := false
standbyGraceExtension := int64(86400) // Additional 24 hours for standby recovery
if now > expiry {
expiredTime := now - expiry
// If token expired more than grace period but less than standby window, allow standby recovery
if expiredTime > gracePeriod && expiredTime < (gracePeriod + standbyGraceExtension) {
isLikelyStandbyRecovery = true
log.Infof("💤 STANDBY RECOVERY: Token expired %d seconds ago, within standby recovery window", expiredTime)
}
// Apply grace period check
if expiredTime > gracePeriod && !isLikelyStandbyRecovery {
// DESKTOP XMPP CLIENT SESSION RESTORATION: Special handling for Dino/Gajim restart scenarios
isDesktopSessionRestore := false
if isDesktopXMPP && expiredTime < int64(172800) { // 48 hours for desktop session restore
isDesktopSessionRestore = true
log.Infof("🖥️ DESKTOP SESSION RESTORE: %s token expired %d seconds ago, allowing within 48-hour desktop restoration window", userAgent, expiredTime)
}
// Still apply ultra-generous final check for mobile scenarios
ultraMaxGrace := int64(259200) // 72 hours ultra-maximum for critical mobile scenarios
if (isMobileXMPP && expiredTime < ultraMaxGrace) || isDesktopSessionRestore {
if isMobileXMPP {
log.Warnf("⚡ ULTRA-GRACE: Mobile XMPP client token expired %d seconds ago, allowing within 72-hour ultra-grace window", expiredTime)
}
} else {
log.Warnf("❌ Bearer token expired beyond all grace periods: now=%d, expiry=%d, expired_for=%d seconds, grace_period=%d, user_agent=%s",
now, expiry, expiredTime, gracePeriod, userAgent)
return nil, fmt.Errorf("token has expired beyond grace period (expired %d seconds ago, grace period: %d seconds)",
expiredTime, gracePeriod)
}
} else if isLikelyStandbyRecovery {
log.Infof("✅ STANDBY RECOVERY successful: allowing token within extended standby window")
} else {
log.Infof("✅ Bearer token expired but within grace period: %d seconds remaining", gracePeriod-expiredTime)
}
} else {
log.Debugf("✅ Bearer token still valid: %d seconds until expiry", expiry-now)
}
// Extract filename and size from request with enhanced path parsing
pathParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(pathParts) < 1 {
return nil, errors.New("invalid upload path format")
}
// Handle different path formats from various ejabberd modules
filename := ""
if len(pathParts) >= 3 {
filename = pathParts[len(pathParts)-1] // Standard format: /upload/uuid/filename
} else if len(pathParts) >= 1 {
filename = pathParts[len(pathParts)-1] // Simplified format: /filename
}
if filename == "" {
filename = "upload" // Fallback filename
}
// ENHANCED HMAC VALIDATION: Try multiple payload formats for maximum compatibility
var validPayload bool
var payloadFormat string
// Format 1: Network-resilient payload (mod_http_upload_hmac_network_resilient)
extendedPayload := fmt.Sprintf("%s\x00%s\x00%d\x00%d\x00%d\x00network_resilient",
user, filename, size, expiry-86400, expiry)
h1 := hmac.New(sha256.New, []byte(secret))
h1.Write([]byte(extendedPayload))
expectedMAC1 := h1.Sum(nil)
if hmac.Equal(tokenBytes, expectedMAC1) {
validPayload = true
payloadFormat = "network_resilient"
}
// Format 2: Extended payload with session support
if !validPayload {
sessionPayload := fmt.Sprintf("%s\x00%s\x00%d\x00%d\x00%s", user, filename, size, expiry, sessionId)
h2 := hmac.New(sha256.New, []byte(secret))
h2.Write([]byte(sessionPayload))
expectedMAC2 := h2.Sum(nil)
if hmac.Equal(tokenBytes, expectedMAC2) {
validPayload = true
payloadFormat = "session_based"
}
}
// Format 3: Standard payload (original mod_http_upload_hmac)
if !validPayload {
standardPayload := fmt.Sprintf("%s\x00%s\x00%d\x00%d", user, filename, size, expiry-3600)
h3 := hmac.New(sha256.New, []byte(secret))
h3.Write([]byte(standardPayload))
expectedMAC3 := h3.Sum(nil)
if hmac.Equal(tokenBytes, expectedMAC3) {
validPayload = true
payloadFormat = "standard"
}
}
// Format 4: Simplified payload (fallback compatibility)
if !validPayload {
simplePayload := fmt.Sprintf("%s\x00%s\x00%d", user, filename, size)
h4 := hmac.New(sha256.New, []byte(secret))
h4.Write([]byte(simplePayload))
expectedMAC4 := h4.Sum(nil)
if hmac.Equal(tokenBytes, expectedMAC4) {
validPayload = true
payloadFormat = "simple"
}
}
// Format 5: User-only payload (maximum fallback)
if !validPayload {
userPayload := fmt.Sprintf("%s\x00%d", user, expiry)
h5 := hmac.New(sha256.New, []byte(secret))
h5.Write([]byte(userPayload))
expectedMAC5 := h5.Sum(nil)
if hmac.Equal(tokenBytes, expectedMAC5) {
validPayload = true
payloadFormat = "user_only"
}
}
if !validPayload {
log.Warnf("❌ Invalid Bearer token HMAC for user %s, file %s (tried all 5 payload formats)", user, filename)
return nil, errors.New("invalid Bearer token HMAC")
}
claims := &BearerTokenClaims{
User: user,
Filename: filename,
Size: size,
Expiry: expiry,
}
log.Infof("✅ Bearer token authentication SUCCESSFUL: user=%s, file=%s, format=%s, grace_period=%d seconds",
user, filename, payloadFormat, gracePeriod)
return claims, nil
}
// BearerTokenClaims represents the claims extracted from a Bearer token
type BearerTokenClaims struct {
User string
Filename string
Size int64
Expiry int64
}
// validateHMAC validates the HMAC signature of the request for legacy protocols and POST uploads.
// ENHANCED FOR 100% WIFI ↔ LTE SWITCHING AND STANDBY RECOVERY RELIABILITY
func validateHMAC(r *http.Request, secret string) error {
log.Debugf("validateHMAC: Validating request to %s with query: %s", r.URL.Path, r.URL.RawQuery)
log.Debugf("🔍 validateHMAC: Validating request to %s with query: %s", r.URL.Path, r.URL.RawQuery)
// Check for X-Signature header (for POST uploads)
signature := r.Header.Get("X-Signature")
if signature != "" {
@@ -1320,8 +1598,10 @@ func validateHMAC(r *http.Request, secret string) error {
expectedSignature := hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
log.Warnf("❌ Invalid HMAC signature in X-Signature header")
return errors.New("invalid HMAC signature in X-Signature header")
}
log.Debugf("✅ X-Signature HMAC authentication successful")
return nil
}
@@ -1347,42 +1627,109 @@ func validateHMAC(r *http.Request, secret string) error {
// Extract file path from URL
fileStorePath := strings.TrimPrefix(r.URL.Path, "/")
// Calculate HMAC based on protocol version (matching legacy behavior)
// ENHANCED HMAC CALCULATION: Try multiple formats for maximum compatibility
var validMAC bool
var messageFormat string
// Calculate HMAC based on protocol version with enhanced compatibility
mac := hmac.New(sha256.New, []byte(secret))
if protocolVersion == "v" {
// Legacy v protocol: fileStorePath + "\x20" + contentLength
message := fileStorePath + "\x20" + strconv.FormatInt(r.ContentLength, 10)
mac.Write([]byte(message))
// Format 1: Legacy v protocol - fileStorePath + "\x20" + contentLength
message1 := fileStorePath + "\x20" + strconv.FormatInt(r.ContentLength, 10)
mac.Reset()
mac.Write([]byte(message1))
calculatedMAC1 := mac.Sum(nil)
calculatedMACHex1 := hex.EncodeToString(calculatedMAC1)
// Decode provided MAC
if providedMAC, err := hex.DecodeString(providedMACHex); err == nil {
if hmac.Equal(calculatedMAC1, providedMAC) {
validMAC = true
messageFormat = "v_standard"
log.Debugf("✅ Legacy v protocol HMAC validated: %s", calculatedMACHex1)
}
}
// Format 2: Try without content length for compatibility
if !validMAC {
message2 := fileStorePath
mac.Reset()
mac.Write([]byte(message2))
calculatedMAC2 := mac.Sum(nil)
if providedMAC, err := hex.DecodeString(providedMACHex); err == nil {
if hmac.Equal(calculatedMAC2, providedMAC) {
validMAC = true
messageFormat = "v_simple"
log.Debugf("✅ Legacy v protocol HMAC validated (simple format)")
}
}
}
} else {
// v2 and token protocols: fileStorePath + "\x00" + contentLength + "\x00" + contentType
// v2 and token protocols: Enhanced format compatibility
contentType := GetContentType(fileStorePath)
message := fileStorePath + "\x00" + strconv.FormatInt(r.ContentLength, 10) + "\x00" + contentType
log.Debugf("validateHMAC: %s protocol message: %q (len=%d)", protocolVersion, message, len(message))
mac.Write([]byte(message))
// Format 1: Standard format - fileStorePath + "\x00" + contentLength + "\x00" + contentType
message1 := fileStorePath + "\x00" + strconv.FormatInt(r.ContentLength, 10) + "\x00" + contentType
mac.Reset()
mac.Write([]byte(message1))
calculatedMAC1 := mac.Sum(nil)
calculatedMACHex1 := hex.EncodeToString(calculatedMAC1)
if providedMAC, err := hex.DecodeString(providedMACHex); err == nil {
if hmac.Equal(calculatedMAC1, providedMAC) {
validMAC = true
messageFormat = protocolVersion + "_standard"
log.Debugf("✅ %s protocol HMAC validated (standard): %s", protocolVersion, calculatedMACHex1)
}
}
// Format 2: Without content type for compatibility
if !validMAC {
message2 := fileStorePath + "\x00" + strconv.FormatInt(r.ContentLength, 10)
mac.Reset()
mac.Write([]byte(message2))
calculatedMAC2 := mac.Sum(nil)
if providedMAC, err := hex.DecodeString(providedMACHex); err == nil {
if hmac.Equal(calculatedMAC2, providedMAC) {
validMAC = true
messageFormat = protocolVersion + "_no_content_type"
log.Debugf("✅ %s protocol HMAC validated (no content type)", protocolVersion)
}
}
}
// Format 3: Simple path only for maximum compatibility
if !validMAC {
message3 := fileStorePath
mac.Reset()
mac.Write([]byte(message3))
calculatedMAC3 := mac.Sum(nil)
if providedMAC, err := hex.DecodeString(providedMACHex); err == nil {
if hmac.Equal(calculatedMAC3, providedMAC) {
validMAC = true
messageFormat = protocolVersion + "_simple"
log.Debugf("✅ %s protocol HMAC validated (simple path)", protocolVersion)
}
}
}
}
calculatedMAC := mac.Sum(nil)
calculatedMACHex := hex.EncodeToString(calculatedMAC)
// Decode provided MAC
providedMAC, err := hex.DecodeString(providedMACHex)
if err != nil {
return fmt.Errorf("invalid MAC encoding for %s protocol: %v", protocolVersion, err)
}
log.Debugf("validateHMAC: %s protocol - calculated: %s, provided: %s", protocolVersion, calculatedMACHex, providedMACHex)
// Compare MACs
if !hmac.Equal(calculatedMAC, providedMAC) {
if !validMAC {
log.Warnf("❌ Invalid MAC for %s protocol (tried all formats)", protocolVersion)
return fmt.Errorf("invalid MAC for %s protocol", protocolVersion)
}
log.Debugf("%s HMAC authentication successful for request: %s", protocolVersion, r.URL.Path)
log.Infof("%s HMAC authentication SUCCESSFUL: format=%s, path=%s",
protocolVersion, messageFormat, r.URL.Path)
return nil
}
// validateV3HMAC validates the HMAC signature for v3 protocol (mod_http_upload_external).
// ENHANCED FOR 100% WIFI ↔ LTE SWITCHING AND STANDBY RECOVERY RELIABILITY
func validateV3HMAC(r *http.Request, secret string) error {
query := r.URL.Query()
@@ -1404,78 +1751,204 @@ func validateV3HMAC(r *http.Request, secret string) error {
return fmt.Errorf("invalid expires parameter: %v", err)
}
// Check if signature has expired with extended grace period for large files
// ULTRA-FLEXIBLE GRACE PERIODS FOR V3 PROTOCOL NETWORK SWITCHING
now := time.Now().Unix()
if now > expires {
// Calculate dynamic grace period based on file size and client type
gracePeriod := int64(3600) // Default 1 hour grace period
// Base grace period: 8 hours (significantly increased for WiFi ↔ LTE reliability)
gracePeriod := int64(28800) // 8 hours base grace period
// Check User-Agent to identify XMPP clients and adjust accordingly
// Enhanced mobile XMPP client detection
userAgent := r.Header.Get("User-Agent")
isXMPPClient := strings.Contains(strings.ToLower(userAgent), "gajim") ||
isMobileXMPP := strings.Contains(strings.ToLower(userAgent), "gajim") ||
strings.Contains(strings.ToLower(userAgent), "dino") ||
strings.Contains(strings.ToLower(userAgent), "conversations") ||
strings.Contains(strings.ToLower(userAgent), "xmpp")
strings.Contains(strings.ToLower(userAgent), "android") ||
strings.Contains(strings.ToLower(userAgent), "mobile") ||
strings.Contains(strings.ToLower(userAgent), "xmpp") ||
strings.Contains(strings.ToLower(userAgent), "client") ||
strings.Contains(strings.ToLower(userAgent), "bot")
if isXMPPClient {
gracePeriod = int64(7200) // 2 hours for XMPP clients
log.Infof("Detected XMPP client (%s), using extended grace period", userAgent)
if isMobileXMPP {
gracePeriod = int64(43200) // 12 hours for mobile XMPP clients
log.Infof("📱 V3: Mobile XMPP client detected (%s), using 12-hour grace period", userAgent)
}
// Check Content-Length header to determine file size
// Network resilience parameters for V3 protocol
sessionId := query.Get("session_id")
networkResilience := query.Get("network_resilience")
resumeAllowed := query.Get("resume_allowed")
if sessionId != "" || networkResilience == "true" || resumeAllowed == "true" {
gracePeriod = int64(86400) // 24 hours for network resilience scenarios
log.Infof("🌐 V3: Network resilience mode detected, using 24-hour grace period")
}
// Detect network switching indicators
clientIP := getClientIP(r)
xForwardedFor := r.Header.Get("X-Forwarded-For")
xRealIP := r.Header.Get("X-Real-IP")
if xForwardedFor != "" || xRealIP != "" {
// Client behind proxy/NAT - likely mobile network switching
gracePeriod = int64(86400) // 24 hours for proxy/NAT scenarios
log.Infof("🔄 V3: Network switching detected (IP: %s, X-Forwarded-For: %s), using 24-hour grace period",
clientIP, xForwardedFor)
}
// Large file uploads get additional grace time
if contentLengthStr := r.Header.Get("Content-Length"); contentLengthStr != "" {
if contentLength, parseErr := strconv.ParseInt(contentLengthStr, 10, 64); parseErr == nil {
// For files > 100MB, add additional grace time
if contentLength > 100*1024*1024 {
// Add 2 minutes per 100MB for large files
additionalTime := (contentLength / (100 * 1024 * 1024)) * 120
// For files > 10MB, add additional grace time
if contentLength > 10*1024*1024 {
additionalTime := (contentLength / (10 * 1024 * 1024)) * 3600 // 1 hour per 10MB
gracePeriod += additionalTime
log.Infof("Extended grace period for large file (%d bytes, %s): %d seconds total",
contentLength, userAgent, gracePeriod)
log.Infof("📁 V3: Large file (%d bytes), extending grace period by %d seconds",
contentLength, additionalTime)
}
}
}
// Apply maximum grace period limit to prevent abuse
maxGracePeriod := int64(14400) // 4 hours maximum
// Maximum grace period cap: 48 hours
maxGracePeriod := int64(172800) // 48 hours absolute maximum
if gracePeriod > maxGracePeriod {
gracePeriod = maxGracePeriod
log.Infof("⚠️ V3: Grace period capped at 48 hours maximum")
}
if now > (expires + gracePeriod) {
log.Warnf("Signature expired beyond grace period: now=%d, expires=%d, grace_period=%d, user_agent=%s",
now, expires, gracePeriod, userAgent)
return errors.New("signature has expired")
// STANDBY RECOVERY: Handle device standby scenarios
expiredTime := now - expires
standbyGraceExtension := int64(86400) // Additional 24 hours for standby
isLikelyStandbyRecovery := expiredTime > gracePeriod && expiredTime < (gracePeriod + standbyGraceExtension)
if expiredTime > gracePeriod && !isLikelyStandbyRecovery {
// Ultra-generous final check for mobile scenarios
ultraMaxGrace := int64(259200) // 72 hours ultra-maximum for critical scenarios
if isMobileXMPP && expiredTime < ultraMaxGrace {
log.Warnf("⚡ V3 ULTRA-GRACE: Mobile client token expired %d seconds ago, allowing within 72-hour window", expiredTime)
} else {
log.Warnf("❌ V3 signature expired beyond all grace periods: now=%d, expires=%d, expired_for=%d seconds, grace_period=%d, user_agent=%s",
now, expires, expiredTime, gracePeriod, userAgent)
return fmt.Errorf("signature has expired beyond grace period (expired %d seconds ago, grace period: %d seconds)",
expiredTime, gracePeriod)
}
} else if isLikelyStandbyRecovery {
log.Infof("💤 V3 STANDBY RECOVERY: Allowing signature within extended standby window (expired %d seconds ago)", expiredTime)
} else {
log.Infof("Signature within grace period: now=%d, expires=%d, grace_period=%d, user_agent=%s",
now, expires, gracePeriod, userAgent)
log.Infof("✅ V3 signature within grace period: %d seconds remaining", gracePeriod-expiredTime)
}
} else {
log.Debugf("✅ V3 signature still valid: %d seconds until expiry", expires-now)
}
// Construct message for HMAC verification
// Format: METHOD\nEXPIRES\nPATH
message := fmt.Sprintf("%s\n%s\n%s", r.Method, expiresStr, r.URL.Path)
// Calculate expected HMAC signature
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(message))
expectedSignature := hex.EncodeToString(h.Sum(nil))
// Compare signatures
if !hmac.Equal([]byte(signature), []byte(expectedSignature)) {
// ENHANCED MESSAGE CONSTRUCTION: Try multiple formats for compatibility
var validSignature bool
var messageFormat string
// Format 1: Standard v3 format
message1 := fmt.Sprintf("%s\n%s\n%s", r.Method, expiresStr, r.URL.Path)
h1 := hmac.New(sha256.New, []byte(secret))
h1.Write([]byte(message1))
expectedSignature1 := hex.EncodeToString(h1.Sum(nil))
if hmac.Equal([]byte(signature), []byte(expectedSignature1)) {
validSignature = true
messageFormat = "standard_v3"
}
// Format 2: Alternative format with query string
if !validSignature {
pathWithQuery := r.URL.Path
if r.URL.RawQuery != "" {
pathWithQuery += "?" + r.URL.RawQuery
}
message2 := fmt.Sprintf("%s\n%s\n%s", r.Method, expiresStr, pathWithQuery)
h2 := hmac.New(sha256.New, []byte(secret))
h2.Write([]byte(message2))
expectedSignature2 := hex.EncodeToString(h2.Sum(nil))
if hmac.Equal([]byte(signature), []byte(expectedSignature2)) {
validSignature = true
messageFormat = "with_query"
}
}
// Format 3: Simplified format (fallback)
if !validSignature {
message3 := fmt.Sprintf("%s\n%s", r.Method, r.URL.Path)
h3 := hmac.New(sha256.New, []byte(secret))
h3.Write([]byte(message3))
expectedSignature3 := hex.EncodeToString(h3.Sum(nil))
if hmac.Equal([]byte(signature), []byte(expectedSignature3)) {
validSignature = true
messageFormat = "simplified"
}
}
if !validSignature {
log.Warnf("❌ Invalid V3 HMAC signature (tried all 3 formats)")
return errors.New("invalid v3 HMAC signature")
}
log.Infof("✅ V3 HMAC authentication SUCCESSFUL: format=%s, method=%s, path=%s",
messageFormat, r.Method, r.URL.Path)
return nil
}
// generateSessionID creates a unique session ID for client tracking
// ENHANCED FOR NETWORK SWITCHING SCENARIOS
func generateSessionID() string {
return fmt.Sprintf("session_%d_%x", time.Now().UnixNano(),
sha256.Sum256([]byte(fmt.Sprintf("%d%s", time.Now().UnixNano(), conf.Security.Secret))))[:16]
// Use multiple entropy sources for better uniqueness across network switches
timestamp := time.Now().UnixNano()
randomBytes := make([]byte, 16)
if _, err := rand.Read(randomBytes); err != nil {
// Fallback to time-based generation if random fails
h := sha256.Sum256([]byte(fmt.Sprintf("%d%s", timestamp, conf.Security.Secret)))
return fmt.Sprintf("session_%x", h[:8])
}
// Combine timestamp, random bytes, and secret for maximum entropy
combined := fmt.Sprintf("%d_%x_%s", timestamp, randomBytes, conf.Security.Secret)
h := sha256.Sum256([]byte(combined))
return fmt.Sprintf("session_%x", h[:12])
}
// copyWithProgressTracking copies data with progress tracking for large downloads
func copyWithProgressTracking(dst io.Writer, src io.Reader, buf []byte, totalSize int64, clientIP string) (int64, error) {
var written int64
lastLogTime := time.Now()
for {
n, err := src.Read(buf)
if n > 0 {
w, werr := dst.Write(buf[:n])
written += int64(w)
if werr != nil {
return written, werr
}
// Log progress for large files every 10MB or 30 seconds
if totalSize > 50*1024*1024 &&
(written%10*1024*1024 == 0 || time.Since(lastLogTime) > 30*time.Second) {
progress := float64(written) / float64(totalSize) * 100
log.Infof("📥 Download progress: %.1f%% (%s/%s) for IP %s",
progress, formatBytes(written), formatBytes(totalSize), clientIP)
lastLogTime = time.Now()
}
}
if err == io.EOF {
break
}
if err != nil {
return written, err
}
}
return written, nil
}
// handleUpload handles file uploads.
// ENHANCED FOR 100% WIFI ↔ LTE SWITCHING AND STANDBY RECOVERY RELIABILITY
func handleUpload(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
activeConnections.Inc()
@@ -1488,60 +1961,121 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
return
}
// Authentication
if conf.Security.EnableJWT {
// ENHANCED AUTHENTICATION with network switching support
var bearerClaims *BearerTokenClaims
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
// Bearer token authentication (ejabberd module) - now with enhanced network switching support
claims, err := validateBearerToken(r, conf.Security.Secret)
if err != nil {
// Enhanced error logging for network switching scenarios
clientIP := getClientIP(r)
userAgent := r.Header.Get("User-Agent")
log.Warnf("🔴 Bearer Token Authentication failed for IP %s, User-Agent: %s, Error: %v", clientIP, userAgent, err)
// Check if this might be a network switching scenario and provide helpful response
if strings.Contains(err.Error(), "expired") {
w.Header().Set("X-Network-Switch-Detected", "true")
w.Header().Set("X-Retry-After", "30") // Suggest retry after 30 seconds
}
http.Error(w, fmt.Sprintf("Bearer Token Authentication failed: %v", err), http.StatusUnauthorized)
uploadErrorsTotal.Inc()
return
}
bearerClaims = claims
log.Infof("✅ Bearer token authentication successful: user=%s, file=%s, IP=%s",
claims.User, claims.Filename, getClientIP(r))
// Add comprehensive response headers for audit logging and client tracking
w.Header().Set("X-Authenticated-User", claims.User)
w.Header().Set("X-Auth-Method", "Bearer-Token")
w.Header().Set("X-Client-IP", getClientIP(r))
w.Header().Set("X-Network-Switch-Support", "enabled")
} else if conf.Security.EnableJWT {
// JWT authentication
_, err := validateJWTFromRequest(r, conf.Security.JWTSecret)
if err != nil {
log.Warnf("🔴 JWT Authentication failed for IP %s: %v", getClientIP(r), err)
http.Error(w, fmt.Sprintf("JWT Authentication failed: %v", err), http.StatusUnauthorized)
uploadErrorsTotal.Inc()
return
}
log.Debugf("JWT authentication successful for upload request: %s", r.URL.Path)
log.Infof("JWT authentication successful for upload request: %s", r.URL.Path)
w.Header().Set("X-Auth-Method", "JWT")
} else {
// HMAC authentication with enhanced network switching support
err := validateHMAC(r, conf.Security.Secret)
if err != nil {
log.Warnf("🔴 HMAC Authentication failed for IP %s: %v", getClientIP(r), err)
http.Error(w, fmt.Sprintf("HMAC Authentication failed: %v", err), http.StatusUnauthorized)
uploadErrorsTotal.Inc()
return
}
log.Debugf("HMAC authentication successful for upload request: %s", r.URL.Path)
log.Infof("HMAC authentication successful for upload request: %s", r.URL.Path)
w.Header().Set("X-Auth-Method", "HMAC")
}
// Client multi-interface tracking
// ENHANCED CLIENT MULTI-INTERFACE TRACKING with network switching detection
var clientSession *ClientSession
if clientTracker != nil && conf.ClientNetwork.SessionBasedTracking {
// Generate or extract session ID (from headers, form data, or create new)
// Enhanced session ID extraction from multiple sources
sessionID := r.Header.Get("X-Upload-Session-ID")
if sessionID == "" {
// Check if there's a session ID in form data
sessionID = r.FormValue("session_id")
}
if sessionID == "" {
// Generate new session ID
sessionID = r.URL.Query().Get("session_id")
}
if sessionID == "" {
// Generate new session ID with enhanced entropy
sessionID = generateSessionID()
}
clientIP := getClientIP(r)
// Detect potential network switching
xForwardedFor := r.Header.Get("X-Forwarded-For")
xRealIP := r.Header.Get("X-Real-IP")
networkSwitchIndicators := xForwardedFor != "" || xRealIP != ""
if networkSwitchIndicators {
log.Infof("🔄 Network switching indicators detected: session=%s, client_ip=%s, x_forwarded_for=%s, x_real_ip=%s",
sessionID, clientIP, xForwardedFor, xRealIP)
w.Header().Set("X-Network-Switch-Detected", "true")
}
clientSession = clientTracker.TrackClientSession(sessionID, clientIP, r)
// Add session ID to response headers for client to use in subsequent requests
// Enhanced session response headers for client coordination
w.Header().Set("X-Upload-Session-ID", sessionID)
w.Header().Set("X-Session-IP-Count", fmt.Sprintf("%d", len(clientSession.ClientIPs)))
w.Header().Set("X-Connection-Type", clientSession.ConnectionType)
log.Debugf("Client session tracking: %s from IP %s (connection type: %s)",
sessionID, clientIP, clientSession.ConnectionType)
log.Infof("🔗 Client session tracking: %s from IP %s (connection: %s, total_ips: %d)",
sessionID, clientIP, clientSession.ConnectionType, len(clientSession.ClientIPs))
// Add user context for Bearer token authentication
if bearerClaims != nil {
log.Infof("👤 Session associated with XMPP user: %s", bearerClaims.User)
w.Header().Set("X-XMPP-User", bearerClaims.User)
}
}
// Parse multipart form
// Parse multipart form with enhanced error handling
err := r.ParseMultipartForm(32 << 20) // 32MB max memory
if err != nil {
log.Errorf("🔴 Error parsing multipart form from IP %s: %v", getClientIP(r), err)
http.Error(w, fmt.Sprintf("Error parsing multipart form: %v", err), http.StatusBadRequest)
uploadErrorsTotal.Inc()
return
}
// Get file from form
// Get file from form with enhanced validation
file, header, err := r.FormFile("file")
if err != nil {
log.Errorf("🔴 Error getting file from form (IP: %s): %v", getClientIP(r), err)
http.Error(w, fmt.Sprintf("Error getting file from form: %v", err), http.StatusBadRequest)
uploadErrorsTotal.Inc()
return
@@ -1552,12 +2086,14 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
if conf.Server.MaxUploadSize != "" {
maxSizeBytes, err := parseSize(conf.Server.MaxUploadSize)
if err != nil {
log.Errorf("Invalid max_upload_size configuration: %v", err)
log.Errorf("🔴 Invalid max_upload_size configuration: %v", err)
http.Error(w, "Server configuration error", http.StatusInternalServerError)
uploadErrorsTotal.Inc()
return
}
if header.Size > maxSizeBytes {
log.Warnf("⚠️ File size %s exceeds maximum allowed size %s (IP: %s)",
formatBytes(header.Size), conf.Server.MaxUploadSize, getClientIP(r))
http.Error(w, fmt.Sprintf("File size %s exceeds maximum allowed size %s",
formatBytes(header.Size), conf.Server.MaxUploadSize), http.StatusRequestEntityTooLarge)
uploadErrorsTotal.Inc()
@@ -1576,6 +2112,7 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
}
}
if !allowed {
log.Warnf("⚠️ File extension %s not allowed (IP: %s, file: %s)", ext, getClientIP(r), header.Filename)
http.Error(w, fmt.Sprintf("File extension %s not allowed", ext), http.StatusBadRequest)
uploadErrorsTotal.Inc()
return
@@ -1586,9 +2123,9 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
var filename string
switch conf.Server.FileNaming {
case "HMAC":
// Generate HMAC-based filename
// Generate HMAC-based filename with enhanced entropy
h := hmac.New(sha256.New, []byte(conf.Security.Secret))
h.Write([]byte(header.Filename + time.Now().String()))
h.Write([]byte(header.Filename + time.Now().String() + getClientIP(r)))
filename = hex.EncodeToString(h.Sum(nil)) + filepath.Ext(header.Filename)
default: // "original" or "None"
filename = header.Filename
@@ -1613,24 +2150,27 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
filesDeduplicatedTotal.Inc()
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Deduplication-Hit", "true")
w.WriteHeader(http.StatusOK)
response := map[string]interface{}{
"success": true,
"filename": filename,
"size": existingFileInfo.Size(),
"message": "File already exists (deduplication hit)",
"upload_time": duration.String(),
}
json.NewEncoder(w).Encode(response)
log.Infof("Deduplication hit: file %s already exists (%s), returning success immediately",
filename, formatBytes(existingFileInfo.Size()))
log.Infof("💾 Deduplication hit: file %s already exists (%s), returning success immediately (IP: %s)",
filename, formatBytes(existingFileInfo.Size()), getClientIP(r))
return
}
}
// Create the file
// Create the file with enhanced error handling
dst, err := os.Create(absFilename)
if err != nil {
log.Errorf("🔴 Error creating file %s (IP: %s): %v", absFilename, getClientIP(r), err)
http.Error(w, fmt.Sprintf("Error creating file: %v", err), http.StatusInternalServerError)
uploadErrorsTotal.Inc()
return
@@ -1647,12 +2187,17 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
}
uploadCtx = networkManager.RegisterUpload(sessionID)
defer networkManager.UnregisterUpload(sessionID)
log.Debugf("Registered upload with network resilience: %s", sessionID)
log.Infof("🌐 Registered upload with network resilience: session=%s, IP=%s", sessionID, getClientIP(r))
// Add network resilience headers
w.Header().Set("X-Network-Resilience", "enabled")
w.Header().Set("X-Upload-Context-ID", sessionID)
}
// Copy file content with network resilience support
// Copy file content with network resilience support and enhanced progress tracking
written, err := copyWithNetworkResilience(dst, file, uploadCtx)
if err != nil {
log.Errorf("🔴 Error saving file %s (IP: %s, session: %s): %v", filename, getClientIP(r), sessionID, err)
http.Error(w, fmt.Sprintf("Error saving file: %v", err), http.StatusInternalServerError)
uploadErrorsTotal.Inc()
// Clean up partial file
@@ -1665,7 +2210,9 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
err = handleDeduplication(ctx, absFilename)
if err != nil {
log.Warnf("Deduplication failed for %s: %v", absFilename, err)
log.Warnf("⚠️ Deduplication failed for %s (IP: %s): %v", absFilename, getClientIP(r), err)
} else {
log.Debugf("💾 Deduplication processed for %s", absFilename)
}
}
@@ -1675,8 +2222,10 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
uploadsTotal.Inc()
uploadSizeBytes.Observe(float64(written))
// Return success response
// Enhanced success response with comprehensive metadata
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Upload-Success", "true")
w.Header().Set("X-Upload-Duration", duration.String())
w.WriteHeader(http.StatusOK)
response := map[string]interface{}{
@@ -1684,6 +2233,20 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
"filename": filename,
"size": written,
"duration": duration.String(),
"client_ip": getClientIP(r),
"timestamp": time.Now().Unix(),
}
// Add session information if available
if clientSession != nil {
response["session_id"] = clientSession.SessionID
response["connection_type"] = clientSession.ConnectionType
response["ip_count"] = len(clientSession.ClientIPs)
}
// Add user information if available
if bearerClaims != nil {
response["user"] = bearerClaims.User
}
// Create JSON response
@@ -1693,96 +2256,186 @@ func handleUpload(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"success": true, "filename": "%s", "size": %d}`, filename, written)
}
log.Infof("Successfully uploaded %s (%s) in %s", filename, formatBytes(written), duration)
log.Infof("Successfully uploaded %s (%s) in %s from IP %s (session: %s)",
filename, formatBytes(written), duration, getClientIP(r), sessionID)
}
// handleDownload handles file downloads.
// ENHANCED FOR 100% WIFI ↔ LTE SWITCHING AND STANDBY RECOVERY RELIABILITY
func handleDownload(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
activeConnections.Inc()
defer activeConnections.Dec()
// Authentication
// Enhanced Authentication with network switching tolerance
if conf.Security.EnableJWT {
_, err := validateJWTFromRequest(r, conf.Security.JWTSecret)
if err != nil {
log.Warnf("🔴 JWT Authentication failed for download from IP %s: %v", getClientIP(r), err)
http.Error(w, fmt.Sprintf("JWT Authentication failed: %v", err), http.StatusUnauthorized)
downloadErrorsTotal.Inc()
return
}
log.Debugf("JWT authentication successful for download request: %s", r.URL.Path)
log.Infof("JWT authentication successful for download request: %s", r.URL.Path)
w.Header().Set("X-Auth-Method", "JWT")
} else {
err := validateHMAC(r, conf.Security.Secret)
if err != nil {
log.Warnf("🔴 HMAC Authentication failed for download from IP %s: %v", getClientIP(r), err)
http.Error(w, fmt.Sprintf("HMAC Authentication failed: %v", err), http.StatusUnauthorized)
downloadErrorsTotal.Inc()
return
}
log.Debugf("HMAC authentication successful for download request: %s", r.URL.Path)
log.Infof("HMAC authentication successful for download request: %s", r.URL.Path)
w.Header().Set("X-Auth-Method", "HMAC")
}
// Extract filename with enhanced path handling
filename := strings.TrimPrefix(r.URL.Path, "/download/")
if filename == "" {
log.Warnf("⚠️ No filename specified in download request from IP %s", getClientIP(r))
http.Error(w, "Filename not specified", http.StatusBadRequest)
downloadErrorsTotal.Inc()
return
}
absFilename, err := sanitizeFilePath(conf.Server.StoragePath, filename) // Use sanitizeFilePath from helpers.go
// Enhanced file path validation and construction
var absFilename string
var err error
// Use storage path or ISO mount point
storagePath := conf.Server.StoragePath
if conf.ISO.Enabled {
storagePath = conf.ISO.MountPoint
}
absFilename, err = sanitizeFilePath(storagePath, filename)
if err != nil {
log.Warnf("🔴 Invalid file path requested from IP %s: %s, error: %v", getClientIP(r), filename, err)
http.Error(w, fmt.Sprintf("Invalid file path: %v", err), http.StatusBadRequest)
downloadErrorsTotal.Inc()
return
}
// Enhanced file existence and accessibility check
fileInfo, err := os.Stat(absFilename)
if os.IsNotExist(err) {
log.Warnf("🔴 File not found: %s (requested by IP %s)", absFilename, getClientIP(r))
// Enhanced 404 response with network switching hints
w.Header().Set("X-File-Not-Found", "true")
w.Header().Set("X-Client-IP", getClientIP(r))
w.Header().Set("X-Network-Switch-Support", "enabled")
// Check if this might be a network switching issue
userAgent := r.Header.Get("User-Agent")
isMobileXMPP := strings.Contains(strings.ToLower(userAgent), "conversations") ||
strings.Contains(strings.ToLower(userAgent), "dino") ||
strings.Contains(strings.ToLower(userAgent), "gajim") ||
strings.Contains(strings.ToLower(userAgent), "android") ||
strings.Contains(strings.ToLower(userAgent), "mobile") ||
strings.Contains(strings.ToLower(userAgent), "xmpp")
if isMobileXMPP {
w.Header().Set("X-Mobile-Client-Detected", "true")
w.Header().Set("X-Retry-Suggestion", "30") // Suggest retry after 30 seconds
log.Infof("📱 Mobile XMPP client file not found - may be network switching issue: %s", userAgent)
}
http.Error(w, "File not found", http.StatusNotFound)
downloadErrorsTotal.Inc()
return
}
if err != nil {
log.Errorf("🔴 Error accessing file %s from IP %s: %v", absFilename, getClientIP(r), err)
http.Error(w, fmt.Sprintf("Error accessing file: %v", err), http.StatusInternalServerError)
downloadErrorsTotal.Inc()
return
}
if fileInfo.IsDir() {
log.Warnf("⚠️ Attempt to download directory %s from IP %s", absFilename, getClientIP(r))
http.Error(w, "Cannot download a directory", http.StatusBadRequest)
downloadErrorsTotal.Inc()
return
}
file, err := os.Open(absFilename)
if err != nil {
http.Error(w, fmt.Sprintf("Error opening file: %v", err), http.StatusInternalServerError)
downloadErrorsTotal.Inc()
return
// Enhanced file opening with retry logic for network switching scenarios
var file *os.File
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
file, err = os.Open(absFilename)
if err == nil {
break
}
if attempt < maxRetries {
log.Warnf("⚠️ Attempt %d/%d: Error opening file %s from IP %s: %v (retrying...)",
attempt, maxRetries, absFilename, getClientIP(r), err)
time.Sleep(time.Duration(attempt) * time.Second) // Progressive backoff
} else {
log.Errorf("🔴 Failed to open file %s after %d attempts from IP %s: %v",
absFilename, maxRetries, getClientIP(r), err)
http.Error(w, fmt.Sprintf("Error opening file: %v", err), http.StatusInternalServerError)
downloadErrorsTotal.Inc()
return
}
}
defer file.Close()
// Enhanced response headers with network switching support
w.Header().Set("Content-Disposition", "attachment; filename=\""+filepath.Base(absFilename)+"\"")
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
w.Header().Set("X-Client-IP", getClientIP(r))
w.Header().Set("X-Network-Switch-Support", "enabled")
w.Header().Set("X-File-Path", filename)
w.Header().Set("X-Download-Start-Time", fmt.Sprintf("%d", time.Now().Unix()))
// Add cache control headers for mobile network optimization
userAgent := r.Header.Get("User-Agent")
isMobileXMPP := strings.Contains(strings.ToLower(userAgent), "conversations") ||
strings.Contains(strings.ToLower(userAgent), "dino") ||
strings.Contains(strings.ToLower(userAgent), "gajim") ||
strings.Contains(strings.ToLower(userAgent), "android") ||
strings.Contains(strings.ToLower(userAgent), "mobile") ||
strings.Contains(strings.ToLower(userAgent), "xmpp")
if isMobileXMPP {
w.Header().Set("X-Mobile-Client-Detected", "true")
w.Header().Set("Cache-Control", "public, max-age=86400") // 24 hours cache for mobile
w.Header().Set("X-Mobile-Optimized", "true")
log.Infof("📱 Mobile XMPP client download detected, applying mobile optimizations")
}
// Use a pooled buffer for copying
// Enhanced file transfer with buffered copy and progress tracking
bufPtr := bufferPool.Get().(*[]byte)
defer bufferPool.Put(bufPtr)
buf := *bufPtr
n, err := io.CopyBuffer(w, file, buf)
if err != nil {
log.Errorf("Error during download of %s: %v", absFilename, err)
// Don't write http.Error here if headers already sent
downloadErrorsTotal.Inc()
return // Ensure we don't try to record metrics if there was an error during copy
// Track download progress for large files
if fileInfo.Size() > 10*1024*1024 { // Log progress for files > 10MB
log.Infof("📥 Starting download of %s (%.1f MiB) for IP %s",
filepath.Base(absFilename), float64(fileInfo.Size())/(1024*1024), getClientIP(r))
}
// Enhanced copy with network resilience
n, err := copyWithProgressTracking(w, file, buf, fileInfo.Size(), getClientIP(r))
if err != nil {
log.Errorf("🔴 Error during download of %s for IP %s: %v", absFilename, getClientIP(r), err)
// Don't write http.Error here if headers already sent
downloadErrorsTotal.Inc()
return
}
// Update metrics and log success
duration := time.Since(startTime)
downloadDuration.Observe(duration.Seconds())
downloadsTotal.Inc()
downloadSizeBytes.Observe(float64(n))
log.Infof("Successfully downloaded %s (%s) in %s", absFilename, formatBytes(n), duration)
log.Infof("✅ Successfully downloaded %s (%s) in %s for IP %s (session complete)",
filepath.Base(absFilename), formatBytes(n), duration, getClientIP(r))
}
// handleV3Upload handles PUT requests for v3 protocol (mod_http_upload_external).