Fixed: NETWORK_RESILIENCE_COMPLETE
This commit is contained in:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user