From f953c2b0bc6d6e038582b93b8195a2427108ead5 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sun, 1 Mar 2026 21:17:27 -0800 Subject: [PATCH] fix(kotlin-sdk): thread-safety, resource leaks, URL encoding, JSON safety, deprecated API - BLTelemetryClient/BLAuditLogger/BLCrashReporter: @Synchronized isoNow() for SimpleDateFormat thread-safety - BLCrashReporter: replace unsafe JSON string interpolation with buildJsonObject + JsonArray - BLBlobClient: close OkHttp Response body with .use {} to prevent resource leak - BLFeatureFlagClient/BLKillSwitchClient: URL-encode query parameter values - build.gradle.kts: kotlinOptions {} -> compilerOptions {} (Kotlin 2.1 convention) --- packages/kotlin-platform-sdk/build.gradle.kts | 4 ++-- .../kotlin/com/bytelyst/platform/BLAuditLogger.kt | 6 +++++- .../kotlin/com/bytelyst/platform/BLBlobClient.kt | 5 +++-- .../com/bytelyst/platform/BLCrashReporter.kt | 15 ++++++++++++--- .../com/bytelyst/platform/BLFeatureFlagClient.kt | 6 ++++-- .../com/bytelyst/platform/BLKillSwitchClient.kt | 4 +++- .../com/bytelyst/platform/BLTelemetryClient.kt | 6 +++++- 7 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/kotlin-platform-sdk/build.gradle.kts b/packages/kotlin-platform-sdk/build.gradle.kts index 6f727402..76109a1f 100644 --- a/packages/kotlin-platform-sdk/build.gradle.kts +++ b/packages/kotlin-platform-sdk/build.gradle.kts @@ -19,8 +19,8 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) } } diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLAuditLogger.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLAuditLogger.kt index 00f3fa28..57fb13be 100644 --- a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLAuditLogger.kt +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLAuditLogger.kt @@ -43,6 +43,10 @@ class BLAuditLogger( timeZone = TimeZone.getTimeZone("UTC") } + /** Thread-safe ISO timestamp — SimpleDateFormat is NOT thread-safe. */ + @Synchronized + private fun isoNow(): String = isoFormat.format(Date()) + init { logDir.mkdirs() } @@ -52,7 +56,7 @@ class BLAuditLogger( */ fun log(action: String, module: String, detail: String? = null, userId: String? = null) { val entry = AuditEntry( - timestamp = isoFormat.format(Date()), + timestamp = isoNow(), productId = config.productId, action = action, module = module, diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLBlobClient.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLBlobClient.kt index b327730f..d4b779ce 100644 --- a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLBlobClient.kt +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLBlobClient.kt @@ -77,8 +77,9 @@ class BLBlobClient( .header("x-ms-blob-content-type", contentType) .build() - val response = uploadClient.newCall(request).execute() - if (response.isSuccessful) sas.blobUrl else null + uploadClient.newCall(request).execute().use { response -> + if (response.isSuccessful) sas.blobUrl else null + } } catch (_: Exception) { null } diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLCrashReporter.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLCrashReporter.kt index fabd0fe9..699b08aa 100644 --- a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLCrashReporter.kt +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLCrashReporter.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import java.io.PrintWriter @@ -42,6 +43,10 @@ class BLCrashReporter( timeZone = TimeZone.getTimeZone("UTC") } + /** Thread-safe ISO timestamp — SimpleDateFormat is NOT thread-safe. */ + @Synchronized + private fun isoNow(): String = isoFormat.format(Date()) + companion object { private const val KEY_PENDING_CRASH = "pending_crash" } @@ -66,7 +71,7 @@ class BLCrashReporter( put("id", UUID.randomUUID().toString()) put("productId", config.productId) put("platform", config.platform) - put("timestamp", isoFormat.format(Date())) + put("timestamp", isoNow()) put("thread", thread.name) put("exception", throwable.javaClass.name) put("message", throwable.message ?: "") @@ -87,8 +92,12 @@ class BLCrashReporter( scope.launch { try { - val body = """{"productId":"${config.productId}","events":[$pending]}""" - client.fireAndForget("POST", "/telemetry/events", body) + val crashEvent = json.parseToJsonElement(pending) + val payload = buildJsonObject { + put("productId", config.productId) + put("events", JsonArray(listOf(crashEvent))) + } + client.fireAndForget("POST", "/telemetry/events", json.encodeToString(payload)) } catch (_: Exception) { // Best-effort — don't re-queue } diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLFeatureFlagClient.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLFeatureFlagClient.kt index 474ad0f9..3797826c 100644 --- a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLFeatureFlagClient.kt +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLFeatureFlagClient.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import java.net.URLEncoder /** * Feature flag client for platform-service. @@ -76,9 +77,10 @@ class BLFeatureFlagClient( private suspend fun fetchFlags() { try { + val enc = { v: String -> URLEncoder.encode(v, "UTF-8") } val qs = buildString { - append("?platform=${config.platform}") - userId?.let { append("&userId=$it") } + append("?platform=${enc(config.platform)}") + userId?.let { append("&userId=${enc(it)}") } } val response = client.request("GET", "/flags/poll$qs", skipAuth = true) val result = json.decodeFromString(response) diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLKillSwitchClient.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLKillSwitchClient.kt index b4c86874..cd7bbdb9 100644 --- a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLKillSwitchClient.kt +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLKillSwitchClient.kt @@ -2,6 +2,7 @@ package com.bytelyst.platform import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import java.net.URLEncoder /** * Kill switch client for platform-service. @@ -32,7 +33,8 @@ class BLKillSwitchClient( */ suspend fun check(): KillSwitchResult { return try { - val response = client.request("GET", "/flags/kill-switch?platform=${config.platform}", skipAuth = true) + val platform = URLEncoder.encode(config.platform, "UTF-8") + val response = client.request("GET", "/flags/kill-switch?platform=$platform", skipAuth = true) json.decodeFromString(response) } catch (_: Exception) { KillSwitchResult.ok() diff --git a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLTelemetryClient.kt b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLTelemetryClient.kt index 8b69c41f..73f977d8 100644 --- a/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLTelemetryClient.kt +++ b/packages/kotlin-platform-sdk/src/main/kotlin/com/bytelyst/platform/BLTelemetryClient.kt @@ -99,6 +99,10 @@ class BLTelemetryClient( timeZone = TimeZone.getTimeZone("UTC") } + /** Thread-safe ISO timestamp — SimpleDateFormat is NOT thread-safe. */ + @Synchronized + private fun isoNow(): String = isoFormat.format(Date()) + // ── Lifecycle ──────────────────────────────────────────── fun start() { @@ -155,7 +159,7 @@ class BLTelemetryClient( errorDomain = errorDomain, tags = tags, metrics = metrics, - occurredAt = isoFormat.format(Date()), + occurredAt = isoNow(), ) synchronized(queue) {