Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ import com.revenuecat.purchases.common.events.EventsManager
import com.revenuecat.purchases.common.isDeviceProtectedStorageCompat
import com.revenuecat.purchases.common.log
import com.revenuecat.purchases.common.networking.ETagManager
import com.revenuecat.purchases.common.networking.HTTPTimeoutManager
import com.revenuecat.purchases.common.offerings.OfferingsCache
import com.revenuecat.purchases.common.offerings.OfferingsFactory
import com.revenuecat.purchases.common.offerings.OfferingsManager
import com.revenuecat.purchases.common.offlineentitlements.OfflineCustomerInfoCalculator
import com.revenuecat.purchases.common.offlineentitlements.OfflineEntitlementsManager
import com.revenuecat.purchases.common.offlineentitlements.PurchasedProductsFetcher
import com.revenuecat.purchases.common.remoteconfig.DefaultRemoteConfigSourceProvider
import com.revenuecat.purchases.common.remoteconfig.RemoteConfigBlobFetcher
import com.revenuecat.purchases.common.remoteconfig.RemoteConfigBlobStore
import com.revenuecat.purchases.common.remoteconfig.RemoteConfigDiskCache
import com.revenuecat.purchases.common.remoteconfig.RemoteConfigManager
Expand Down Expand Up @@ -212,6 +214,7 @@ internal class PurchasesFactory(
}
val apiSourceProvider = DefaultRemoteConfigSourceProvider(remoteConfigTopicStore)

val timeoutManager = HTTPTimeoutManager(appConfig)
val httpClient = HTTPClient(
appConfig,
eTagManager,
Expand All @@ -221,6 +224,7 @@ internal class PurchasesFactory(
apiSourceProvider,
localeProvider = localeProvider,
forceServerErrorStrategy = forceServerErrorStrategy,
timeoutManager = timeoutManager,
)
val backendHelper = BackendHelper(apiKey, backendDispatcher, appConfig, httpClient)
val backend = Backend(
Expand Down Expand Up @@ -287,12 +291,14 @@ internal class PurchasesFactory(
)

val remoteConfigManager = if (remoteConfigDiskCache != null) {
val remoteConfigBlobStore = RemoteConfigBlobStore(contextForStorage)
RemoteConfigManager(
backend = backend,
diskCache = remoteConfigDiskCache,
blobStore = RemoteConfigBlobStore(contextForStorage),
blobStore = remoteConfigBlobStore,
topicStore = remoteConfigTopicStore,
sourceProvider = apiSourceProvider,
blobFetcher = RemoteConfigBlobFetcher(remoteConfigBlobStore, apiSourceProvider, timeoutManager),
// Bootstrap source for a cold on-demand read's self-triggered sync (see blobData()); after
// the first identity change the manager syncs for the user clearCache() binds instead.
appUserIDProvider = { cache.getCachedAppUserID() },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.revenuecat.purchases.common.remoteconfig

import com.revenuecat.purchases.common.errorLog
import com.revenuecat.purchases.common.networking.HTTPTimeoutManager
import com.revenuecat.purchases.common.remoteconfig.RemoteConfigSourceHandle.Purpose
import com.revenuecat.purchases.common.verboseLog
import com.revenuecat.purchases.utils.DefaultUrlConnectionFactory
Expand All @@ -17,6 +18,9 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import java.io.IOException
import java.net.HttpURLConnection
import java.net.MalformedURLException
import java.net.SocketTimeoutException
import java.net.URL
import java.nio.ByteBuffer
import java.util.PriorityQueue

Expand Down Expand Up @@ -45,6 +49,7 @@ private const val BLOB_REF_PLACEHOLDER = "{blob_ref}"
internal class RemoteConfigBlobFetcher(
private val blobStore: RemoteConfigBlobStore,
private val sourceProvider: RemoteConfigSourceProvider,
private val timeoutManager: HTTPTimeoutManager,
private val urlConnectionFactory: UrlConnectionFactory = DefaultUrlConnectionFactory(),
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
) {
Expand Down Expand Up @@ -208,11 +213,20 @@ internal class RemoteConfigBlobFetcher(
}

private fun tryDownloadVerifyStore(url: String, ref: String): DownloadOutcome {
val host = hostOf(url)
val timeout = timeoutManager.getTimeoutForRequest(
host = host,
isFallback = false,
endpointSupportsFallbackURLs = false,
isProxied = false,
).toInt()

var connection: UrlConnection? = null
return try {
connection = urlConnectionFactory.createConnection(url)
connection = urlConnectionFactory.createConnection(url, timeout, timeout)
when (val code = connection.responseCode) {
HttpURLConnection.HTTP_OK -> {
timeoutManager.recordRequestResult(host, HTTPTimeoutManager.RequestResult.SUCCESS_ON_MAIN_BACKEND)
val bytes = connection.inputStream.use { it.readBytes() }
verifyAndStore(bytes, ref, url)
}
Expand All @@ -225,6 +239,10 @@ internal class RemoteConfigBlobFetcher(
DownloadOutcome.SOURCE_UNHEALTHY
}
}
} catch (e: SocketTimeoutException) {
errorLog(e) { "Timed out downloading remote config blob '$ref' from $url." }
timeoutManager.recordRequestResult(host, HTTPTimeoutManager.RequestResult.MAIN_SOURCE_TIMED_OUT)
DownloadOutcome.SOURCE_UNHEALTHY
} catch (e: IOException) {
errorLog(e) { "Failed to download remote config blob '$ref' from $url." }
DownloadOutcome.SOURCE_UNHEALTHY
Expand All @@ -233,6 +251,13 @@ internal class RemoteConfigBlobFetcher(
}
}

private fun hostOf(url: String): String? = try {
URL(url).host
} catch (e: MalformedURLException) {
verboseLog { "Could not resolve host for remote config blob URL $url: ${e.message}" }
null
}

private fun verifyAndStore(bytes: ByteArray, ref: String, url: String): DownloadOutcome {
if (RemoteConfigUtils.contentAddressRef(bytes) != ref) {
errorLog { "Remote config blob '$ref' from $url failed content-address verification." }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal class RemoteConfigManager(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val topicStore: RemoteConfigTopicStore,
private val sourceProvider: RemoteConfigSourceProvider,
private val blobFetcher: RemoteConfigBlobFetcher = RemoteConfigBlobFetcher(blobStore, sourceProvider),
private val blobFetcher: RemoteConfigBlobFetcher,
private val appUserIDProvider: () -> String? = { null },
) {
private val isRefreshing = AtomicBoolean(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.revenuecat.purchases.common.currentLogHandler
import com.revenuecat.purchases.common.verboseLog
import com.revenuecat.purchases.models.Checksum
import com.revenuecat.purchases.models.toHexString
import com.revenuecat.purchases.utils.DEFAULT_CONNECTION_TIMEOUT_MS
import com.revenuecat.purchases.utils.DefaultUrlConnectionFactory
import com.revenuecat.purchases.utils.UrlConnection
import com.revenuecat.purchases.utils.UrlConnectionFactory
Expand Down Expand Up @@ -130,7 +131,11 @@ internal class DefaultFileRepository(
private fun downloadFile(url: URL): UrlConnection = try {
verboseLog { "Downloading remote file from $url" }

val connection = urlConnectionFactory.createConnection(url.toString())
val connection = urlConnectionFactory.createConnection(
url.toString(),
DEFAULT_CONNECTION_TIMEOUT_MS,
DEFAULT_CONNECTION_TIMEOUT_MS,
)

if (connection.responseCode != HttpURLConnection.HTTP_OK) {
connection.disconnect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import java.net.URL
import java.security.MessageDigest

internal interface UrlConnectionFactory {
fun createConnection(url: String, requestMethod: String = "GET"): UrlConnection
fun createConnection(
url: String,
connectTimeoutMillis: Int,
readTimeoutMillis: Int,
requestMethod: String = "GET",
): UrlConnection
}

internal interface UrlConnection {
Expand All @@ -24,13 +29,19 @@ internal interface UrlConnection {
fun disconnect()
}

private const val TIMEOUT = 5000
/** Default connect/read timeout for downloads that don't tune it per attempt. */
internal const val DEFAULT_CONNECTION_TIMEOUT_MS = 5000

internal class DefaultUrlConnectionFactory : UrlConnectionFactory {
override fun createConnection(url: String, requestMethod: String): UrlConnection {
override fun createConnection(
url: String,
connectTimeoutMillis: Int,
readTimeoutMillis: Int,
requestMethod: String,
): UrlConnection {
val connection = URL(url).openConnection() as HttpURLConnection
connection.connectTimeout = TIMEOUT
connection.readTimeout = TIMEOUT
connection.connectTimeout = connectTimeoutMillis
connection.readTimeout = readTimeoutMillis
connection.requestMethod = requestMethod
connection.doInput = true
return DefaultUrlConnection(connection)
Expand Down Expand Up @@ -60,7 +71,7 @@ internal fun UrlConnectionFactory.downloadToFile(
verboseLog { "Downloading $description from $url" }
var connection: UrlConnection? = null
try {
connection = createConnection(url)
connection = createConnection(url, DEFAULT_CONNECTION_TIMEOUT_MS, DEFAULT_CONNECTION_TIMEOUT_MS)
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw IOException(
"HTTP ${connection.responseCode} when downloading $description from $url",
Expand Down Expand Up @@ -88,7 +99,7 @@ internal fun UrlConnectionFactory.downloadToFileAndVerifyChecksum(
val digest = MessageDigest.getInstance(expectedChecksum.algorithm.algorithmName)
var connection: UrlConnection? = null
try {
connection = createConnection(url)
connection = createConnection(url, DEFAULT_CONNECTION_TIMEOUT_MS, DEFAULT_CONNECTION_TIMEOUT_MS)
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw IOException(
"HTTP ${connection.responseCode} when downloading $description from $url",
Expand Down
Loading