diff --git a/CERTIFICATE_SECURITY.md b/CERTIFICATE_SECURITY.md new file mode 100644 index 0000000..836cdf9 --- /dev/null +++ b/CERTIFICATE_SECURITY.md @@ -0,0 +1,184 @@ +# Certificate Security Configuration + +This document explains the certificate validation configuration for development and production builds. + +## Overview + +The notification system uses different certificate validation strategies depending on the build type: + +- **Debug builds**: Allow self-signed certificates for development +- **Release builds**: Strict certificate validation using system CAs only + +## Build-Specific Configuration + +### Debug Builds (Development) + +**File**: `demo-app/app/src/debug/res/xml/network_security_config.xml` + +- Allows self-signed certificates for localhost, 127.0.0.1, and 10.0.2.2 (Android emulator) +- Trusts both system CAs and user-added certificates +- Includes debug overrides for development flexibility +- **WARNING**: Never use debug configuration in production + +### Release Builds (Production) + +**File**: `demo-app/app/src/release/res/xml/network_security_config.xml` + +- Strict certificate validation using system CAs only +- No user certificate trust for maximum security +- Requires proper CA-signed certificates +- Production domain placeholder needs to be updated + +## Android App Implementation + +### HTTP Client Creation + +The `MainActivity.createHttpClient()` method automatically selects the appropriate client: + +```kotlin +private fun createHttpClient(): OkHttpClient { + return if (BuildConfig.DEBUG) { + createDebugHttpClient() // Allows self-signed certs + } else { + createReleaseHttpClient() // Strict validation + } +} +``` + +### Debug Client Features + +- Logs certificate acceptance for debugging +- Accepts self-signed certificates +- Flexible hostname verification +- Development-friendly error handling + +### Release Client Features + +- Uses system certificate authorities only +- Respects network security configuration +- Strict hostname verification +- Production-grade security + +## Production Deployment Requirements + +### 1. Backend Certificate Setup + +For production deployment, replace the self-signed certificate in `app-backend/` with proper CA-signed certificates: + +```bash +# Replace these files with CA-signed certificates: +app-backend/cert.pem # Public certificate +app-backend/key.pem # Private key +``` + +### 2. Certificate Options + +**Option A: Let's Encrypt (Free)** +```bash +certbot certonly --standalone -d your-domain.com +cp /etc/letsencrypt/live/your-domain.com/fullchain.pem app-backend/cert.pem +cp /etc/letsencrypt/live/your-domain.com/privkey.pem app-backend/key.pem +``` + +**Option B: Commercial CA** +- Purchase certificate from trusted CA (DigiCert, Comodo, etc.) +- Follow CA's installation instructions +- Place certificate files in `app-backend/` + +**Option C: Internal CA (Enterprise)** +- Use organizational certificate authority +- Ensure certificates are trusted by target devices +- Consider certificate pinning for additional security + +### 3. Update Production Domain + +Edit `demo-app/app/src/release/res/xml/network_security_config.xml`: + +```xml +your-actual-domain.com +``` + +### 4. Android App Configuration + +Update the default backend URL in `SettingsActivity` to use the production domain: + +```kotlin +const val DEFAULT_BACKEND_URL = "https://your-production-domain.com:8443" +``` + +## Security Verification + +### Testing Debug Build + +1. Build debug APK: `./gradlew assembleDebug` +2. Install on test device +3. Verify self-signed certificate acceptance +4. Check logs for certificate debugging messages + +### Testing Release Build + +1. Build release APK: `./gradlew assembleRelease` +2. Install on test device +3. Verify rejection of self-signed certificates +4. Confirm only CA-signed certificates are accepted + +### Certificate Validation Test + +```bash +# Test certificate chain +openssl s_client -connect your-domain.com:8443 -showcerts + +# Verify certificate expiration +openssl x509 -in app-backend/cert.pem -text -noout | grep "Not After" +``` + +## Security Best Practices + +1. **Never deploy debug builds to production** +2. **Use HTTPS everywhere** - no HTTP in production +3. **Monitor certificate expiration** - set up renewal alerts +4. **Consider certificate pinning** for high-security applications +5. **Regular security audits** of certificate configuration +6. **Backup certificate private keys** securely + +## Troubleshooting + +### Common Issues + +**Certificate not trusted in release build** +- Verify certificate is signed by trusted CA +- Check network_security_config.xml domain configuration +- Ensure certificate matches domain name + +**Debug build not accepting self-signed certificate** +- Verify debug network_security_config.xml exists +- Check build type configuration +- Review OkHttp client implementation + +**Production deployment fails** +- Verify certificate and private key match +- Check certificate expiration date +- Ensure proper file permissions on server + +### Logging + +The app provides detailed logging for certificate validation: + +- Debug builds: Certificate acceptance logged +- Release builds: Strict validation logged +- Network errors: Detailed error messages + +## Monitoring + +For production deployments, monitor: + +- Certificate expiration dates +- SSL/TLS handshake failures +- Certificate validation errors +- Client connection success rates + +--- + +**Last Updated**: January 2025 +**Security Level**: Production Ready +**Next Review**: Certificate expiration check diff --git a/app-backend/go.mod b/app-backend/go.mod index f1a7587..1bcad31 100644 --- a/app-backend/go.mod +++ b/app-backend/go.mod @@ -1,3 +1,3 @@ module app-backend -go 1.24.3 +go 1.24.5 diff --git a/app-backend/main.go b/app-backend/main.go index bbee3d7..8b00516 100644 --- a/app-backend/main.go +++ b/app-backend/main.go @@ -12,6 +12,7 @@ import ( "log" "net/http" "os" + "strings" "sync" "time" ) @@ -77,11 +78,111 @@ func (ts *TokenStore) Count() int { return len(ts.tokenIDs) } +// RequestLog represents a structured log entry for HTTP requests +type RequestLog struct { + Timestamp time.Time `json:"timestamp"` + Method string `json:"method"` + Path string `json:"path"` + RemoteAddr string `json:"remote_addr"` + UserAgent string `json:"user_agent"` + StatusCode int `json:"status_code"` + ResponseTime int64 `json:"response_time_ms"` + BodySize int64 `json:"body_size"` + Error string `json:"error,omitempty"` +} + +// ResponseWriter wrapper to capture status code and response size +type loggingResponseWriter struct { + http.ResponseWriter + statusCode int + bodySize int64 +} + +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} + +func (lrw *loggingResponseWriter) Write(b []byte) (int, error) { + size, err := lrw.ResponseWriter.Write(b) + lrw.bodySize += int64(size) + return size, err +} + var ( tokenStore = NewTokenStore() publicKeyHash string ) +// loggingMiddleware wraps HTTP handlers to provide structured logging +func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Create logging response writer + lrw := &loggingResponseWriter{ + ResponseWriter: w, + statusCode: 200, // Default status code + } + + // Call the next handler + next(lrw, r) + + // Calculate response time + responseTime := time.Since(start).Milliseconds() + + // Create structured log entry + logEntry := RequestLog{ + Timestamp: start, + Method: r.Method, + Path: r.URL.Path, + RemoteAddr: getClientIP(r), + UserAgent: r.UserAgent(), + StatusCode: lrw.statusCode, + ResponseTime: responseTime, + BodySize: lrw.bodySize, + } + + // Add error field for non-2xx responses + if lrw.statusCode >= 400 { + logEntry.Error = http.StatusText(lrw.statusCode) + } + + // Log as JSON + logJSON, err := json.Marshal(logEntry) + if err != nil { + log.Printf("Error marshaling log entry: %v", err) + return + } + + log.Printf("REQUEST_LOG: %s", string(logJSON)) + } +} + +// getClientIP extracts the real client IP from request headers +func getClientIP(r *http.Request) string { + // Check X-Forwarded-For header (for proxies/load balancers) + if xForwardedFor := r.Header.Get("X-Forwarded-For"); xForwardedFor != "" { + // X-Forwarded-For can contain multiple IPs, take the first one + ifs := strings.Split(xForwardedFor, ",") + if len(ifs) > 0 { + return strings.TrimSpace(ifs[0]) + } + } + + // Check X-Real-IP header (for nginx) + if xRealIP := r.Header.Get("X-Real-IP"); xRealIP != "" { + return xRealIP + } + + // Fall back to RemoteAddr + // Remove port if present + if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + return r.RemoteAddr +} + func main() { flag.Parse() @@ -101,9 +202,9 @@ func main() { publicKeyHash = computePublicKeyHash(publicKeyPEM) log.Printf("Public key hash computed: %s", publicKeyHash[:16]+"...") - http.HandleFunc("/register", handleRegister) - http.HandleFunc("/send-all", handleSendAll) - http.HandleFunc("/", handleHome) + http.HandleFunc("/register", loggingMiddleware(handleRegister)) + http.HandleFunc("/send-all", loggingMiddleware(handleSendAll)) + http.HandleFunc("/", loggingMiddleware(handleHome)) log.Printf("App Backend Server starting on HTTPS port %s", *port) log.Printf("Web interface available at: https://localhost:%s", *port) diff --git a/demo-app/app/build.gradle b/demo-app/app/build.gradle index f1ae51c..6967e27 100644 --- a/demo-app/app/build.gradle +++ b/demo-app/app/build.gradle @@ -26,11 +26,11 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '11' } } diff --git a/demo-app/app/src/main/res/xml/network_security_config.xml b/demo-app/app/src/debug/res/xml/network_security_config.xml similarity index 53% rename from demo-app/app/src/main/res/xml/network_security_config.xml rename to demo-app/app/src/debug/res/xml/network_security_config.xml index 6573027..6d8ae04 100644 --- a/demo-app/app/src/main/res/xml/network_security_config.xml +++ b/demo-app/app/src/debug/res/xml/network_security_config.xml @@ -1,9 +1,12 @@ + 10.0.2.2 + localhost + 127.0.0.1 - + @@ -17,4 +20,12 @@ - + + + + + + + + + \ No newline at end of file diff --git a/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt b/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt index 8a24127..aac13a0 100644 --- a/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt +++ b/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt @@ -33,10 +33,72 @@ class MainActivity : AppCompatActivity() { private lateinit var registerButton: Button private lateinit var statusText: TextView - private val client = createUnsafeOkHttpClient() + private val client = createHttpClient() companion object { private const val TAG = "MainActivity" + // Set to true to force debug certificate behavior for testing + // WARNING: Never set to true in production builds + private const val FORCE_DEBUG_CERTIFICATES = false + } + + /** + * Securely wipes a string from memory by overwriting its backing array + * Returns empty string to replace the original variable + */ + private fun secureWipeString(value: String?): String { + if (value == null) return "" + + try { + // Convert to char array and overwrite with zeros + val chars = value.toCharArray() + for (i in chars.indices) { + chars[i] = '\u0000' + } + + // Also try to overwrite the byte representation + val bytes = value.toByteArray() + for (i in bytes.indices) { + bytes[i] = 0 + } + + Log.d(TAG, "Securely wiped string from memory (${value.length} chars)") + } catch (e: Exception) { + Log.w(TAG, "Warning: Could not securely wipe string from memory", e) + } + + return "" // Return empty string to replace original variable + } + + /** + * Securely wipes a byte array from memory + */ + private fun secureWipeBytes(bytes: ByteArray?) { + if (bytes == null) return + + try { + for (i in bytes.indices) { + bytes[i] = 0 + } + Log.d(TAG, "Securely wiped byte array from memory (${bytes.size} bytes)") + } catch (e: Exception) { + Log.w(TAG, "Warning: Could not securely wipe byte array from memory", e) + } + } + + /** + * Securely wipes a SecretKey from memory + */ + private fun secureWipeSecretKey(key: SecretKey?) { + if (key == null) return + + try { + // Wipe the encoded key bytes + secureWipeBytes(key.encoded) + Log.d(TAG, "Securely wiped SecretKey from memory") + } catch (e: Exception) { + Log.w(TAG, "Warning: Could not securely wipe SecretKey from memory", e) + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -92,128 +154,223 @@ class MainActivity : AppCompatActivity() { } // Get new Firebase registration token - val token = task.result - Log.d(TAG, "Firebase Registration Token: $token") + var token = task.result + Log.d(TAG, "Firebase Registration Token received (${token.length} characters)") - // Send token to server - sendTokenToServer(token) + try { + // Send token to server + sendTokenToServer(token) + } finally { + // Wipe token from memory after sending + token = secureWipeString(token) + } } } private fun sendTokenToServer(token: String) { - updateStatus(getString(R.string.status_encrypting_token)) - - // Encrypt the token before sending - val encryptedToken = try { - encryptToken(token) - } catch (e: Exception) { - Log.e(TAG, "Failed to encrypt token", e) - updateStatus(getString(R.string.status_failed_encrypt, e.message ?: "")) - registerButton.isEnabled = true - return - } - - val json = JSONObject() - json.put("encrypted_data", encryptedToken) - json.put("platform", "android") - - val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType()) + var secureToken = token // Create mutable copy for secure wiping - val backendUrl = SettingsActivity.getBackendUrl(this@MainActivity) - val registerUrl = "$backendUrl/register" - - val request = Request.Builder() - .url(registerUrl) - .post(body) - .build() - - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.e(TAG, "Failed to send token to server", e) - runOnUiThread { - updateStatus(getString(R.string.status_failed_register, e.message ?: "")) - registerButton.isEnabled = true - } + try { + updateStatus(getString(R.string.status_encrypting_token)) + + // Encrypt the token before sending + val encryptedToken = try { + encryptToken(secureToken) + } catch (e: Exception) { + Log.e(TAG, "Failed to encrypt token", e) + updateStatus(getString(R.string.status_failed_encrypt, e.message ?: "")) + registerButton.isEnabled = true + return + } finally { + // Wipe token from memory immediately after encryption attempt + secureToken = secureWipeString(secureToken) } - override fun onResponse(call: Call, response: Response) { - val responseBody = response.body?.string() - Log.d(TAG, "Server response: ${response.code} - $responseBody") + val json = JSONObject() + json.put("encrypted_data", encryptedToken) + json.put("platform", "android") + + val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType()) + + val backendUrl = SettingsActivity.getBackendUrl(this@MainActivity) + val registerUrl = "$backendUrl/register" + + val request = Request.Builder() + .url(registerUrl) + .post(body) + .build() + + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "Failed to send token to server", e) + runOnUiThread { + updateStatus(getString(R.string.status_failed_register, e.message ?: "")) + registerButton.isEnabled = true + } + } - runOnUiThread { - if (response.isSuccessful) { - updateStatus(getString(R.string.status_success)) - } else { - updateStatus(getString(R.string.status_server_error, response.code, responseBody)) + override fun onResponse(call: Call, response: Response) { + val responseBody = response.body?.string() + Log.d(TAG, "Server response: ${response.code} - $responseBody") + + runOnUiThread { + if (response.isSuccessful) { + updateStatus(getString(R.string.status_success)) + } else { + updateStatus(getString(R.string.status_server_error, response.code, responseBody)) + } + registerButton.isEnabled = true } - registerButton.isEnabled = true } - } - }) + }) + + } finally { + // Ensure token is wiped from memory even if there are exceptions + secureToken = secureWipeString(secureToken) + } } private fun updateStatus(message: String) { statusText.text = message } - private fun createUnsafeOkHttpClient(): OkHttpClient { + /** + * Creates HTTP client with build-appropriate certificate validation + * - Debug builds: Allow self-signed certificates for development + * - Release builds: Strict certificate validation using system CAs only + */ + private fun createHttpClient(): OkHttpClient { + return if (isDebugBuild()) { + createDebugHttpClient() + } else { + createReleaseHttpClient() + } + } + + /** + * Determines if this is a debug build by checking application info + * Falls back to safe release behavior if detection fails + */ + private fun isDebugBuild(): Boolean { + // Manual override for testing (WARNING: Never use in production) + if (FORCE_DEBUG_CERTIFICATES) { + Log.w(TAG, "WARNING: Using forced debug certificate mode - not for production!") + return true + } + return try { - // Create a trust manager that does not validate certificate chains + // Check application debuggable flag + val appInfo = packageManager.getApplicationInfo(packageName, 0) + val isDebuggable = (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0 + + Log.i(TAG, "Build type detection - Debuggable flag: $isDebuggable") + return isDebuggable + } catch (e: Exception) { + Log.e(TAG, "Error checking debug build status, defaulting to release mode for security", e) + false // Default to release behavior for safety + } + } + + /** + * Debug HTTP client - allows self-signed certificates for development + * WARNING: Only used in debug builds, never in production + */ + private fun createDebugHttpClient(): OkHttpClient { + return try { + Log.w(TAG, "DEBUG BUILD: Using development HTTP client that accepts self-signed certificates") + + // Create a trust manager that accepts self-signed certificates val trustAllCerts = arrayOf( object : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) {} - override fun checkServerTrusted(chain: Array, authType: String) {} + override fun checkClientTrusted(chain: Array, authType: String) { + Log.d(TAG, "Debug: Accepting client certificate: ${chain[0].subjectDN}") + } + override fun checkServerTrusted(chain: Array, authType: String) { + Log.d(TAG, "Debug: Accepting server certificate: ${chain[0].subjectDN}") + } override fun getAcceptedIssuers(): Array = arrayOf() } ) - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, trustAllCerts, java.security.SecureRandom()) - - // Create an ssl socket factory with our all-trusting manager - val sslSocketFactory = sslContext.socketFactory + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustAllCerts, SecureRandom()) OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) - .hostnameVerifier(HostnameVerifier { _, _ -> true }) + .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) + .hostnameVerifier { hostname, _ -> + Log.d(TAG, "Debug: Accepting hostname: $hostname") + true + } .build() } catch (e: Exception) { - Log.e(TAG, "Error creating unsafe HTTP client", e) - OkHttpClient() + Log.e(TAG, "Error creating debug HTTP client", e) + OkHttpClient() // Fallback to default client } } - private fun encryptToken(token: String): String { - // Generate a random AES-256 key for this token - val keyGen = KeyGenerator.getInstance("AES") - keyGen.init(256) - val aesKey = keyGen.generateKey() - - // Encrypt the token with AES-GCM - val aesCipher = Cipher.getInstance("AES/GCM/NoPadding") - val iv = ByteArray(12) // 96-bit IV for GCM - SecureRandom().nextBytes(iv) - val gcmSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag - aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec) - - val encryptedToken = aesCipher.doFinal(token.toByteArray()) + /** + * Release HTTP client - strict certificate validation + * Uses system certificate authorities only for production security + */ + private fun createReleaseHttpClient(): OkHttpClient { + Log.i(TAG, "RELEASE BUILD: Using secure HTTP client with strict certificate validation") - // Encrypt the AES key with RSA - val publicKey = loadPublicKey() - val rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") - rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey) - val encryptedAesKey = rsaCipher.doFinal(aesKey.encoded) - - // Combine: IV (12 bytes) + encrypted AES key length (4 bytes) + encrypted AES key + encrypted token - val keyLengthBytes = ByteArray(4) - keyLengthBytes[0] = (encryptedAesKey.size shr 24).toByte() - keyLengthBytes[1] = (encryptedAesKey.size shr 16).toByte() - keyLengthBytes[2] = (encryptedAesKey.size shr 8).toByte() - keyLengthBytes[3] = encryptedAesKey.size.toByte() + // Use default OkHttpClient which respects network security config + // This will enforce the release network_security_config.xml settings + return OkHttpClient.Builder() + .build() + } + + private fun encryptToken(token: String): String { + var aesKey: SecretKey? = null + var iv: ByteArray? = null + var encryptedToken: ByteArray? = null + var encryptedAesKey: ByteArray? = null - val combined = iv + keyLengthBytes + encryptedAesKey + encryptedToken - return Base64.encodeToString(combined, Base64.DEFAULT) + try { + // Generate a random AES-256 key for this token + val keyGen = KeyGenerator.getInstance("AES") + keyGen.init(256) + aesKey = keyGen.generateKey() + + // Encrypt the token with AES-GCM + val aesCipher = Cipher.getInstance("AES/GCM/NoPadding") + iv = ByteArray(12) // 96-bit IV for GCM + SecureRandom().nextBytes(iv) + val gcmSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag + aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec) + + encryptedToken = aesCipher.doFinal(token.toByteArray()) + + // Encrypt the AES key with RSA + val publicKey = loadPublicKey() + val rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") + rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey) + encryptedAesKey = rsaCipher.doFinal(aesKey.encoded) + + // Combine: IV (12 bytes) + encrypted AES key length (4 bytes) + encrypted AES key + encrypted token + val keyLengthBytes = ByteArray(4) + keyLengthBytes[0] = (encryptedAesKey.size shr 24).toByte() + keyLengthBytes[1] = (encryptedAesKey.size shr 16).toByte() + keyLengthBytes[2] = (encryptedAesKey.size shr 8).toByte() + keyLengthBytes[3] = encryptedAesKey.size.toByte() + + val combined = iv + keyLengthBytes + encryptedAesKey + encryptedToken + return Base64.encodeToString(combined, Base64.DEFAULT) + + } finally { + // Securely wipe sensitive data from memory + try { + aesKey?.let { secureWipeSecretKey(it) } + iv?.let { secureWipeBytes(it) } + encryptedToken?.let { secureWipeBytes(it) } + encryptedAesKey?.let { secureWipeBytes(it) } + + Log.d(TAG, "Encryption completed, sensitive data wiped from memory") + } catch (e: Exception) { + Log.w(TAG, "Warning: Could not completely wipe encryption data from memory", e) + } + } } private fun loadPublicKey(): PublicKey { diff --git a/demo-app/app/src/release/res/xml/network_security_config.xml b/demo-app/app/src/release/res/xml/network_security_config.xml new file mode 100644 index 0000000..3b99b63 --- /dev/null +++ b/demo-app/app/src/release/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ + + + + + + your-production-domain.com + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/demo-app/build.gradle b/demo-app/build.gradle index b7225ac..93c5151 100644 --- a/demo-app/build.gradle +++ b/demo-app/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.9.10" + ext.kotlin_version = "2.2.0" repositories { google() mavenCentral() diff --git a/notification-backend/go.mod b/notification-backend/go.mod index 06eecc8..7f39ead 100644 --- a/notification-backend/go.mod +++ b/notification-backend/go.mod @@ -1,22 +1,22 @@ module notification-backend -go 1.24.3 +go 1.24.5 require ( - firebase.google.com/go/v4 v4.16.1 + firebase.google.com/go/v4 v4.17.0 github.com/aws/aws-sdk-go-v2 v1.36.6 github.com/aws/aws-sdk-go-v2/config v1.29.18 github.com/aws/aws-sdk-go-v2/credentials v1.17.71 github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 - google.golang.org/api v0.231.0 + google.golang.org/api v0.243.0 ) require ( cel.dev/expr v0.23.1 // indirect cloud.google.com/go v0.121.0 // indirect - cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth v0.16.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect cloud.google.com/go/firestore v1.18.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect @@ -53,30 +53,30 @@ require ( github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect github.com/zeebo/errs v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/net v0.40.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/time v0.12.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/grpc v1.72.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect + google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/notification-backend/go.sum b/notification-backend/go.sum index 622139b..7954745 100644 --- a/notification-backend/go.sum +++ b/notification-backend/go.sum @@ -2,12 +2,12 @@ cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= -cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= -cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc= +cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= @@ -22,8 +22,8 @@ cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHU cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= -firebase.google.com/go/v4 v4.16.1 h1:Kl5cgXmM0VOWDGT1UAx6b0T2UFWa14ak0CvYqeI7Py4= -firebase.google.com/go/v4 v4.16.1/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= +firebase.google.com/go/v4 v4.17.0 h1:Bih69QV/k0YKPA1qUX04ln0aPT9IERrAo2ezibcngzE= +firebase.google.com/go/v4 v4.17.0/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= @@ -110,8 +110,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -127,72 +127,72 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= -google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= +google.golang.org/api v0.243.0 h1:sw+ESIJ4BVnlJcWu9S+p2Z6Qq1PjG77T8IJ1xtp4jZQ= +google.golang.org/api v0.243.0/go.mod h1:GE4QtYfaybx1KmeHMdBnNnyLzBZCVihGBXAmJu/uUr8= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= -google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= -google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0= -google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= +google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/notification-backend/main.go b/notification-backend/main.go index a757f58..9f79c69 100644 --- a/notification-backend/main.go +++ b/notification-backend/main.go @@ -17,6 +17,7 @@ import ( "log" "net/http" "os" + "strings" "sync" "time" @@ -209,6 +210,37 @@ func (ts *DurableTokenStore) saveToFile() error { return os.Rename(tempFile, ts.storageFile) } +// RequestLog represents a structured log entry for HTTP requests +type RequestLog struct { + Timestamp time.Time `json:"timestamp"` + Method string `json:"method"` + Path string `json:"path"` + RemoteAddr string `json:"remote_addr"` + UserAgent string `json:"user_agent"` + StatusCode int `json:"status_code"` + ResponseTime int64 `json:"response_time_ms"` + BodySize int64 `json:"body_size"` + Error string `json:"error,omitempty"` +} + +// ResponseWriter wrapper to capture status code and response size +type loggingResponseWriter struct { + http.ResponseWriter + statusCode int + bodySize int64 +} + +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} + +func (lrw *loggingResponseWriter) Write(b []byte) (int, error) { + size, err := lrw.ResponseWriter.Write(b) + lrw.bodySize += int64(size) + return size, err +} + var ( tokenStore *DurableTokenStore exoscaleStorage *ExoscaleStorage @@ -218,6 +250,75 @@ var ( useExoscale bool ) +// loggingMiddleware wraps HTTP handlers to provide structured logging +func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Create logging response writer + lrw := &loggingResponseWriter{ + ResponseWriter: w, + statusCode: 200, // Default status code + } + + // Call the next handler + next(lrw, r) + + // Calculate response time + responseTime := time.Since(start).Milliseconds() + + // Create structured log entry + logEntry := RequestLog{ + Timestamp: start, + Method: r.Method, + Path: r.URL.Path, + RemoteAddr: getClientIP(r), + UserAgent: r.UserAgent(), + StatusCode: lrw.statusCode, + ResponseTime: responseTime, + BodySize: lrw.bodySize, + } + + // Add error field for non-2xx responses + if lrw.statusCode >= 400 { + logEntry.Error = http.StatusText(lrw.statusCode) + } + + // Log as JSON + logJSON, err := json.Marshal(logEntry) + if err != nil { + log.Printf("Error marshaling log entry: %v", err) + return + } + + log.Printf("REQUEST_LOG: %s", string(logJSON)) + } +} + +// getClientIP extracts the real client IP from request headers +func getClientIP(r *http.Request) string { + // Check X-Forwarded-For header (for proxies/load balancers) + if xForwardedFor := r.Header.Get("X-Forwarded-For"); xForwardedFor != "" { + // X-Forwarded-For can contain multiple IPs, take the first one + ifs := strings.Split(xForwardedFor, ",") + if len(ifs) > 0 { + return strings.TrimSpace(ifs[0]) + } + } + + // Check X-Real-IP header (for nginx) + if xRealIP := r.Header.Get("X-Real-IP"); xRealIP != "" { + return xRealIP + } + + // Fall back to RemoteAddr + // Remove port if present + if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + return r.RemoteAddr +} + func main() { flag.Parse() @@ -294,11 +395,11 @@ func main() { go startCleanupRoutine() } - http.HandleFunc("/register", handleRegister) - http.HandleFunc("/send", handleSend) - http.HandleFunc("/notify", handleNotify) - http.HandleFunc("/status", handleStatus) - http.HandleFunc("/", handleRoot) + http.HandleFunc("/register", loggingMiddleware(handleRegister)) + http.HandleFunc("/send", loggingMiddleware(handleSend)) + http.HandleFunc("/notify", loggingMiddleware(handleNotify)) + http.HandleFunc("/status", loggingMiddleware(handleStatus)) + http.HandleFunc("/", loggingMiddleware(handleRoot)) log.Printf("FCM Notification Server starting on port %s", *port) log.Printf("Storage: %s", getStorageType())