diff --git a/.gitignore b/.gitignore
index 93656fe16..46a7ce5bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,5 +25,6 @@ output
composeApp/src/desktopMain/resources/windows/WinSparkle.*
/composeApp/src/desktopMain/frameworks/
+*.mmdb
certificates/*.pem
diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index 7dd18d0ab..c6082661d 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -290,8 +290,8 @@ android {
coreLibraryDesugaring(libs.android.desugar.jdk)
debugImplementation(compose.uiTooling)
"fullImplementation"(libs.bundles.full.android)
- "fullImplementation"("org.ooni:oonimkall:3.27.0-android:@aar")
- "fdroidImplementation"("org.ooni:oonimkall:3.27.0-android:@aar")
+ "fullImplementation"("org.ooni:oonimkall:3.28.0-alpha-android:@aar")
+ "fdroidImplementation"("org.ooni:oonimkall:3.28.0-alpha-android:@aar")
"xperimentalImplementation"(files("libs/android-oonimkall.aar"))
androidTestUtil(libs.android.orchestrator)
}
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt b/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt
index 42aa3c951..628526599 100644
--- a/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt
+++ b/composeApp/src/androidMain/kotlin/org/ooni/engine/AndroidOonimkallBridge.kt
@@ -63,6 +63,10 @@ class AndroidOonimkallBridge : OonimkallBridge {
it.softwareVersion = softwareVersion
it.assetsDir = assetsDir
+ // geoipDB may not exist in Android binding; set reflectively if available
+ geoIpDB?.let { path ->
+ it.geoipDB = path
+ }
it.stateDir = stateDir
it.tempDir = tempDir
it.tunnelDir = tunnelDir
diff --git a/composeApp/src/androidMain/kotlin/org/ooni/probe/net/Http.android.kt b/composeApp/src/androidMain/kotlin/org/ooni/probe/net/Http.android.kt
new file mode 100644
index 000000000..9a44f640d
--- /dev/null
+++ b/composeApp/src/androidMain/kotlin/org/ooni/probe/net/Http.android.kt
@@ -0,0 +1,24 @@
+package org.ooni.probe.net
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.net.HttpURLConnection
+import java.net.URL
+
+actual suspend fun httpGetBytes(url: String): ByteArray =
+ withContext(Dispatchers.IO) {
+ val connection = (URL(url).openConnection() as HttpURLConnection)
+ connection.requestMethod = "GET"
+ connection.instanceFollowRedirects = true
+ connection.connectTimeout = 15000
+ connection.readTimeout = 30000
+ try {
+ val code = connection.responseCode
+ val stream = if (code in 200..299) connection.inputStream else connection.errorStream
+ val bytes = stream?.use { it.readBytes() } ?: ByteArray(0)
+ if (code !in 200..299) throw RuntimeException("HTTP $code while GET $url: ${String(bytes)}")
+ bytes
+ } finally {
+ connection.disconnect()
+ }
+ }
diff --git a/composeApp/src/commonMain/composeResources/values/strings-untranslatable.xml b/composeApp/src/commonMain/composeResources/values/strings-untranslatable.xml
index e32289531..5ab2153ba 100644
--- a/composeApp/src/commonMain/composeResources/values/strings-untranslatable.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings-untranslatable.xml
@@ -16,4 +16,5 @@
2160p (4k)
%1$s %2$s
+ 20250801
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
index c21c0298f..4f1650864 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
@@ -148,7 +148,7 @@ class Engine(
private fun session(sessionConfig: OonimkallBridge.SessionConfig): OonimkallBridge.Session = bridge.newSession(sessionConfig)
- private fun buildTaskSettings(
+ private suspend fun buildTaskSettings(
netTest: NetTest,
taskOrigin: TaskOrigin,
preferences: EnginePreferences,
@@ -166,6 +166,7 @@ class Engine(
tunnelDir = "$baseFilePath/tunnel",
tempDir = cacheDir,
assetsDir = "$baseFilePath/assets",
+ geoIpDB = preferences.geoipDbVersion?.let { "$cacheDir/$it.mmdb" },
options = TaskSettings.Options(
noCollector = !preferences.uploadResults,
softwareName = buildSoftwareName(taskOrigin),
@@ -196,7 +197,7 @@ class Engine(
MAX_RUNTIME_DISABLED
}
- private fun buildSessionConfig(
+ private suspend fun buildSessionConfig(
taskOrigin: TaskOrigin,
preferences: EnginePreferences,
) = OonimkallBridge.SessionConfig(
@@ -208,6 +209,7 @@ class Engine(
tunnelDir = "$baseFilePath/tunnel",
tempDir = cacheDir,
assetsDir = "$baseFilePath/assets",
+ geoIpDB = preferences.geoipDbVersion?.let { "$cacheDir/$it.mmdb" },
logger = oonimkallLogger,
verbose = false,
)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt
index 8322430c5..6fa4f3ab4 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/OonimkallBridge.kt
@@ -31,6 +31,7 @@ interface OonimkallBridge {
val proxy: String?,
val probeServicesURL: String?,
val assetsDir: String,
+ val geoIpDB: String?,
val stateDir: String,
val tempDir: String,
val tunnelDir: String,
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt
index 1aa1010b0..4d46f191a 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/TaskEventMapper.kt
@@ -96,9 +96,15 @@ class TaskEventMapper(
asn = value?.probeAsn,
ip = value?.probeIp,
countryCode = value?.probeCc,
+ geoIpdb = value?.geoIpdb,
networkType = networkTypeFinder(),
)
+ "status.resolver_lookup" -> value?.geoIpdb?.let {
+ println(it)
+ null
+ }
+
"status.measurement_done" ->
TaskEvent.MeasurementDone(index = value?.idx ?: 0)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/EnginePreferences.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/EnginePreferences.kt
index 29346b6f0..27baf062d 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/EnginePreferences.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/EnginePreferences.kt
@@ -7,5 +7,6 @@ data class EnginePreferences(
val taskLogLevel: TaskLogLevel,
val uploadResults: Boolean,
val proxy: String?,
+ val geoipDbVersion: String?,
val maxRuntime: Duration?,
)
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt
index ef28c7519..a91a3e324 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEvent.kt
@@ -15,6 +15,7 @@ sealed interface TaskEvent {
val ip: String?,
val asn: String?,
val countryCode: String?,
+ val geoIpdb: String?,
val networkType: NetworkType,
) : TaskEvent
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt
index 169390d21..3f199718d 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskEventResult.kt
@@ -26,6 +26,8 @@ data class TaskEventResult(
var idx: Int = 0,
@SerialName("report_id")
var reportId: String? = null,
+ @SerialName("geoip_db")
+ var geoIpdb: String? = null,
@SerialName("probe_ip")
var probeIp: String? = null,
@SerialName("probe_asn")
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskSettings.kt b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskSettings.kt
index d4e22a965..e1e6b67e5 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskSettings.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/engine/models/TaskSettings.kt
@@ -15,6 +15,7 @@ data class TaskSettings(
@SerialName("temp_dir") val tempDir: String,
@SerialName("tunnel_dir") val tunnelDir: String,
@SerialName("assets_dir") val assetsDir: String,
+ @SerialName("geoip_db") val geoIpDB: String?,
@SerialName("options") val options: Options,
@SerialName("annotations") val annotations: Annotations,
// Optional
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
index 01ff8e68f..aa4f95b47 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
@@ -119,6 +119,10 @@ fun App(
// ForegroundServiceDidNotStartInTimeException some users are getting
// dependencies.startSingleRunInner(RunSpecification.OnlyUploadMissingResults)
}
+ LaunchedEffect(Unit) {
+ // Check for GeoIP DB updates in the background
+ runCatching { dependencies.fetchGeoIpDbUpdates() }
+ }
LaunchedEffect(Unit) {
dependencies.observeAndConfigureAutoUpdate()
}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt
index a96f31af9..4cea5acc6 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/models/SettingsKey.kt
@@ -65,6 +65,8 @@ enum class SettingsKey(
DELETE_UPLOADED_JSONS("deleteUploadedJsons"),
IS_NOTIFICATION_DIALOG("isNotificationDialog"),
FIRST_RUN("first_run"),
+ MMDB_VERSION("mmdb_version"),
+ MMDB_LAST_CHECK("mmdb_last_check"),
CHOSEN_WEBSITES("chosen_websites"),
DESCRIPTOR_SECTIONS_COLLAPSED("descriptor_sections_collapsed"),
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt
index ac65480b1..c2bd6609f 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/data/repositories/PreferenceRepository.kt
@@ -75,11 +75,13 @@ class PreferenceRepository(
): PreferenceKey<*> {
val preferenceKey = getPreferenceKey(name = key.value, prefix = prefix, autoRun = autoRun)
return when (key) {
+ SettingsKey.MMDB_LAST_CHECK,
SettingsKey.MAX_RUNTIME,
SettingsKey.LEGACY_PROXY_PORT,
SettingsKey.DELETE_OLD_RESULTS_THRESHOLD,
-> PreferenceKey.IntKey(intPreferencesKey(preferenceKey))
+ SettingsKey.MMDB_VERSION,
SettingsKey.LEGACY_PROXY_HOSTNAME,
SettingsKey.LEGACY_PROXY_PROTOCOL,
SettingsKey.PROXY_SELECTED,
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
index e189ec077..37cada078 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/di/Dependencies.kt
@@ -17,6 +17,7 @@ import org.ooni.engine.Engine
import org.ooni.engine.NetworkTypeFinder
import org.ooni.engine.OonimkallBridge
import org.ooni.engine.TaskEventMapper
+import org.ooni.probe.net.httpGetBytes
import org.ooni.probe.Database
import org.ooni.probe.background.RunBackgroundTask
import org.ooni.probe.config.BatteryOptimization
@@ -50,7 +51,9 @@ import org.ooni.probe.domain.ClearStorage
import org.ooni.probe.domain.DeleteMeasurementsWithoutResult
import org.ooni.probe.domain.DeleteOldResults
import org.ooni.probe.domain.DeleteResults
+import org.ooni.probe.domain.DownloadFile
import org.ooni.probe.domain.DownloadUrls
+import org.ooni.probe.domain.FetchGeoIpDbUpdates
import org.ooni.probe.domain.FinishInProgressData
import org.ooni.probe.domain.GetAutoRunSettings
import org.ooni.probe.domain.GetAutoRunSpecification
@@ -116,6 +119,7 @@ import org.ooni.probe.ui.settings.proxy.ProxyViewModel
import org.ooni.probe.ui.settings.webcategories.WebCategoriesViewModel
import org.ooni.probe.ui.upload.UploadMeasurementsViewModel
import kotlin.coroutines.CoroutineContext
+import kotlin.getValue
class Dependencies(
val platformInfo: PlatformInfo,
@@ -201,9 +205,25 @@ class Dependencies(
}
// Engine
-
private val taskEventMapper by lazy { TaskEventMapper(networkTypeFinder, json) }
+ private val downloader by lazy {
+ DownloadFile(
+ fileSystem = FileSystem.SYSTEM,
+ fetchBytes = { url -> httpGetBytes(url) },
+ )
+ }
+
+ val fetchGeoIpDbUpdates by lazy {
+ FetchGeoIpDbUpdates(
+ downloadFile = downloader::invoke,
+ cacheDir = cacheDir,
+ engineHttpDo = engine::httpDo,
+ json = json,
+ preferencesRepository = preferenceRepository,
+ )
+ }
+
@VisibleForTesting
val engine by lazy {
Engine(
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/DownloadFile.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/DownloadFile.kt
new file mode 100644
index 000000000..9ea2e25d9
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/DownloadFile.kt
@@ -0,0 +1,32 @@
+package org.ooni.probe.domain
+
+import okio.FileSystem
+import okio.Path
+import okio.Path.Companion.toPath
+import okio.buffer
+import okio.use
+
+/**
+ * Downloads binary content to a target absolute path using the provided fetcher.
+ * - Creates parent directories if needed
+ * - Skips writing if the target already exists with the same size
+ */
+class DownloadFile(
+ private val fileSystem: FileSystem,
+ private val fetchBytes: suspend (url: String) -> ByteArray,
+) {
+ suspend operator fun invoke(
+ url: String,
+ absoluteTargetPath: String,
+ ): Path {
+ val target = absoluteTargetPath.toPath()
+ target.parent?.let { parent ->
+ if (fileSystem.metadataOrNull(parent) == null) fileSystem.createDirectories(parent)
+ }
+ val bytes = fetchBytes(url)
+ val existing = fileSystem.metadataOrNull(target)
+ if (existing?.size == bytes.size.toLong()) return target
+ fileSystem.sink(target).buffer().use { sink -> sink.write(bytes) }
+ return target
+ }
+}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchGeoIpDbUpdates.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchGeoIpDbUpdates.kt
new file mode 100644
index 000000000..0ca9ebdfe
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/FetchGeoIpDbUpdates.kt
@@ -0,0 +1,82 @@
+package org.ooni.probe.domain
+
+import kotlinx.coroutines.flow.first
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import okio.Path
+import ooniprobe.composeapp.generated.resources.Res
+import ooniprobe.composeapp.generated.resources.engine_mmdb_version
+import org.jetbrains.compose.resources.getString
+import org.ooni.engine.Engine
+import org.ooni.engine.models.Failure
+import org.ooni.engine.models.Result
+import org.ooni.engine.models.Success
+import org.ooni.engine.models.TaskOrigin
+import org.ooni.probe.data.models.SettingsKey
+import org.ooni.probe.data.repositories.PreferenceRepository
+import kotlin.time.Clock
+
+class FetchGeoIpDbUpdates(
+ private val downloadFile: suspend (url: String, absoluteTargetPath: String) -> Path,
+ private val cacheDir: String,
+ private val engineHttpDo: suspend (method: String, url: String, taskOrigin: TaskOrigin) -> Result,
+ private val preferencesRepository: PreferenceRepository,
+ private val json: Json,
+) {
+ suspend operator fun invoke(): Result =
+ try {
+ when (val versionRes = getLatestEngineVersion()) {
+ is Failure -> Failure(versionRes.reason)
+ is Success -> {
+ val (isLatest, _, latestVersion) = isGeoIpDbLatest(versionRes.value)
+ if (isLatest) {
+ Success(null)
+ } else {
+ val versionName = latestVersion
+ val url = buildGeoIpDbUrl(versionName)
+ val target = "$cacheDir/$versionName.mmdb"
+
+ downloadFile(url, target).let {
+ preferencesRepository.setValueByKey(SettingsKey.MMDB_VERSION, versionName)
+ preferencesRepository.setValueByKey(SettingsKey.MMDB_LAST_CHECK, Clock.System.now().toEpochMilliseconds())
+ Success(it)
+ }
+ }
+ }
+ }
+ } catch (t: Throwable) {
+ Failure(t as? Engine.MkException ?: Engine.MkException(t))
+ }
+
+ /**
+ * Compare latest and current version integers and return pair of latest state and actual version number
+ * @return Triple where the first element is true if the DB is the latest,
+ * the second is the current version and the third is the latest version.
+ */
+ private suspend fun isGeoIpDbLatest(latestVersion: String): Triple {
+ val currentGeoIpDbVersion: String =
+ (preferencesRepository.getValueByKey(SettingsKey.MMDB_VERSION).first() ?: getString(Res.string.engine_mmdb_version)) as String
+
+ return Triple(normalize(currentGeoIpDbVersion) >= normalize(latestVersion), currentGeoIpDbVersion, latestVersion)
+ }
+
+ private suspend fun getLatestEngineVersion(): Result {
+ val url = "https://api.0.github.com/repos/aanorbel/oomplt-mmdb/releases/latest"
+
+ return engineHttpDo("GET", url, TaskOrigin.OoniRun).map { payload ->
+ val jsonStr = payload ?: throw Engine.MkException(Throwable("Empty body"))
+ json.decodeFromString(GhRelease.serializer(), jsonStr).tag
+ }
+ }
+
+ private fun buildGeoIpDbUrl(version: String): String =
+ "https://github.com/aanorbel/oomplt-mmdb/releases/download/$version/$version-ip2country_as.mmdb"
+
+ private fun normalize(tag: String): Int = tag.removePrefix("v").trim().toInt()
+
+ @Serializable
+ data class GhRelease(
+ @SerialName("tag_name") val tag: String,
+ )
+}
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetEnginePreferences.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetEnginePreferences.kt
index ae26803d1..aa4e3fbf4 100644
--- a/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetEnginePreferences.kt
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/domain/GetEnginePreferences.kt
@@ -29,6 +29,7 @@ class GetEnginePreferences(
null
},
proxy = getProxyOption().first().value,
+ geoipDbVersion = getValueForKey(SettingsKey.MMDB_VERSION) as String?,
)
private suspend fun getEnabledCategories(): List {
diff --git a/composeApp/src/commonMain/kotlin/org/ooni/probe/net/Http.kt b/composeApp/src/commonMain/kotlin/org/ooni/probe/net/Http.kt
new file mode 100644
index 000000000..eff1ae8f1
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/org/ooni/probe/net/Http.kt
@@ -0,0 +1,7 @@
+package org.ooni.probe.net
+
+/**
+ * Perform a simple HTTP GET and return the raw response body bytes.
+ * Implemented per-platform to ensure binary-safe downloads.
+ */
+expect suspend fun httpGetBytes(url: String): ByteArray
diff --git a/composeApp/src/desktopMain/kotlin/org/ooni/engine/DesktopOonimkallBridge.kt b/composeApp/src/desktopMain/kotlin/org/ooni/engine/DesktopOonimkallBridge.kt
index 8b82dbddc..8a515789f 100644
--- a/composeApp/src/desktopMain/kotlin/org/ooni/engine/DesktopOonimkallBridge.kt
+++ b/composeApp/src/desktopMain/kotlin/org/ooni/engine/DesktopOonimkallBridge.kt
@@ -64,6 +64,10 @@ class DesktopOonimkallBridge : OonimkallBridge {
it.softwareVersion = softwareVersion
it.assetsDir = assetsDir
+ // geoipDB may or may not exist in this binding; set via reflection when available
+ geoIpDB?.let { path ->
+ // it.geoipDB = path
+ }
it.stateDir = stateDir
it.tempDir = tempDir
it.tunnelDir = tunnelDir
diff --git a/composeApp/src/desktopMain/kotlin/org/ooni/probe/net/Http.desktop.kt b/composeApp/src/desktopMain/kotlin/org/ooni/probe/net/Http.desktop.kt
new file mode 100644
index 000000000..89a741d12
--- /dev/null
+++ b/composeApp/src/desktopMain/kotlin/org/ooni/probe/net/Http.desktop.kt
@@ -0,0 +1,25 @@
+package org.ooni.probe.net
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.BufferedInputStream
+import java.net.HttpURLConnection
+import java.net.URL
+
+actual suspend fun httpGetBytes(url: String): ByteArray =
+ withContext(Dispatchers.IO) {
+ val connection = (URL(url).openConnection() as HttpURLConnection)
+ connection.requestMethod = "GET"
+ connection.instanceFollowRedirects = true
+ connection.connectTimeout = 15000
+ connection.readTimeout = 30000
+ try {
+ val code = connection.responseCode
+ val stream = if (code in 200..299) connection.inputStream else connection.errorStream
+ val bytes = stream?.let { BufferedInputStream(it).use { bis -> bis.readBytes() } } ?: ByteArray(0)
+ if (code !in 200..299) throw RuntimeException("HTTP $code while GET $url: ${bytes.decodeToString()}")
+ bytes
+ } finally {
+ connection.disconnect()
+ }
+ }
diff --git a/composeApp/src/iosMain/kotlin/org/ooni/probe/net/Http.ios.kt b/composeApp/src/iosMain/kotlin/org/ooni/probe/net/Http.ios.kt
new file mode 100644
index 000000000..4e211154e
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/org/ooni/probe/net/Http.ios.kt
@@ -0,0 +1,43 @@
+package org.ooni.probe.net
+
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
+import platform.Foundation.NSData
+import platform.Foundation.NSURL
+import platform.Foundation.NSURLSession
+import platform.Foundation.dataTaskWithURL
+import platform.posix.memcpy
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+actual suspend fun httpGetBytes(url: String): ByteArray =
+ suspendCancellableCoroutine { cont ->
+ val nsurl = NSURL.URLWithString(url)!!
+ val task = NSURLSession.sharedSession.dataTaskWithURL(nsurl) { data, response, error ->
+ when {
+ error != null -> cont.resumeWithException(RuntimeException(error.localizedDescription))
+ data != null -> {
+ // If we have an HTTP response, check status code
+ val http = response as? platform.Foundation.NSHTTPURLResponse
+ val status = http?.statusCode?.toInt() ?: 200
+ if (status in 200..299) {
+ cont.resume((data as NSData).toByteArray())
+ } else {
+ cont.resumeWithException(RuntimeException("HTTP $status while GET $url"))
+ }
+ }
+ else -> cont.resume(ByteArray(0))
+ }
+ }
+ cont.invokeOnCancellation { task.cancel() }
+ task.resume()
+ }
+
+private fun NSData.toByteArray(): ByteArray {
+ val result = ByteArray(length.toInt())
+ result.usePinned {
+ memcpy(it.addressOf(0), this.bytes, this.length)
+ }
+ return result
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 57684a7ae..e02d145b2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -105,7 +105,7 @@ fastlane-screengrab = { module = "tools.fastlane:screengrab", version = "2.1.1"
auto-launch = { module = "io.github.vinceglb:auto-launch", version = "0.7.0"}
directories = { module = "dev.dirs:directories", version = "26" }
pratanumandal-unique = { module = "tk.pratanumandal:unique4j", version = "1.4" }
-desktop-oonimkall = { module = "org.ooni:oonimkall", version = "3.27.0-desktop" }
+desktop-oonimkall = { module = "org.ooni:oonimkall", version = "3.28.0-alpha-desktop" }
[bundles]
diff --git a/iosApp/Podfile b/iosApp/Podfile
index af4d741f2..34fbf1908 100644
--- a/iosApp/Podfile
+++ b/iosApp/Podfile
@@ -2,7 +2,7 @@ platform :ios, '14.0'
use_frameworks!
def shared_pods
- ooni_version = "v3.27.0"
+ ooni_version = "v3.28.0-alpha"
ooni_pods_location = "https://github.com/ooni/probe-cli/releases/download/#{ooni_version}"
pod 'composeApp', :path => '../composeApp'
diff --git a/iosApp/Podfile.lock b/iosApp/Podfile.lock
index 2451b03d5..a59c0f93b 100644
--- a/iosApp/Podfile.lock
+++ b/iosApp/Podfile.lock
@@ -1,12 +1,12 @@
PODS:
- composeApp (1.0.0):
- Sentry (= 8.55.1)
- - libcrypto (2025.09.09-110535)
- - libevent (2025.09.09-110535)
- - libssl (2025.09.09-110535)
- - libtor (2025.09.09-110535)
- - libz (2025.09.09-110535)
- - oonimkall (2025.09.09-110535)
+ - libcrypto (2025.10.24-063517)
+ - libevent (2025.10.24-063517)
+ - libssl (2025.10.24-063517)
+ - libtor (2025.10.24-063517)
+ - libz (2025.10.24-063517)
+ - oonimkall (2025.10.24-063517)
- Sentry (8.55.1):
- Sentry/Core (= 8.55.1)
- Sentry/Core (8.55.1)
@@ -17,12 +17,12 @@ PODS:
DEPENDENCIES:
- composeApp (from `../composeApp`)
- - libcrypto (from `https://github.com/ooni/probe-cli/releases/download/v3.27.0/libcrypto.podspec`)
- - libevent (from `https://github.com/ooni/probe-cli/releases/download/v3.27.0/libevent.podspec`)
- - libssl (from `https://github.com/ooni/probe-cli/releases/download/v3.27.0/libssl.podspec`)
- - libtor (from `https://github.com/ooni/probe-cli/releases/download/v3.27.0/libtor.podspec`)
- - libz (from `https://github.com/ooni/probe-cli/releases/download/v3.27.0/libz.podspec`)
- - oonimkall (from `https://github.com/ooni/probe-cli/releases/download/v3.27.0/oonimkall.podspec`)
+ - libcrypto (from `https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libcrypto.podspec`)
+ - libevent (from `https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libevent.podspec`)
+ - libssl (from `https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libssl.podspec`)
+ - libtor (from `https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libtor.podspec`)
+ - libz (from `https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libz.podspec`)
+ - oonimkall (from `https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/oonimkall.podspec`)
- Siren
- sqlite3 (~> 3.42.0)
@@ -36,30 +36,30 @@ EXTERNAL SOURCES:
composeApp:
:path: "../composeApp"
libcrypto:
- :podspec: https://github.com/ooni/probe-cli/releases/download/v3.27.0/libcrypto.podspec
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libcrypto.podspec
libevent:
- :podspec: https://github.com/ooni/probe-cli/releases/download/v3.27.0/libevent.podspec
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libevent.podspec
libssl:
- :podspec: https://github.com/ooni/probe-cli/releases/download/v3.27.0/libssl.podspec
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libssl.podspec
libtor:
- :podspec: https://github.com/ooni/probe-cli/releases/download/v3.27.0/libtor.podspec
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libtor.podspec
libz:
- :podspec: https://github.com/ooni/probe-cli/releases/download/v3.27.0/libz.podspec
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/libz.podspec
oonimkall:
- :podspec: https://github.com/ooni/probe-cli/releases/download/v3.27.0/oonimkall.podspec
+ :podspec: https://github.com/ooni/probe-cli/releases/download/v3.28.0-alpha/oonimkall.podspec
SPEC CHECKSUMS:
composeApp: 23f1c8946d30f151e633bdcf3c7db996ae3be9ce
- libcrypto: 6fe6cdcad3c473ed4b3ccfe12a95f30840de48e7
- libevent: c8df42a11d8217584f940cdfe4aed60fd056572e
- libssl: 7d9f469af78e11cb2b207211b85bcd415d903ae8
- libtor: 08056abb8cd5fa1c7c7e5cd21e22e4436b699b73
- libz: d695d2d4082e5b71e6a988188eb8b5b2b18b43fc
- oonimkall: 9d00aecca34685d6fd6252139703be7793bb6ba8
+ libcrypto: a95cf1d71053abe5d1ffbf286a1897f52c6999e2
+ libevent: 457557e55295bffdf1bbac3c5c832639dfa149fe
+ libssl: 595044ab6ea6bf038641ac044b1782bf0fdbfbc1
+ libtor: b68e0b20fb994a7d5447a0e5e09739c10d1b8643
+ libz: 3ae34fb1e45f0e43457a2e456c0d3dd63a8f72f0
+ oonimkall: 4082e113ff788b56d36e3459a1ef51c21c3434b0
Sentry: 6c92b12db0634612f6a66757890fea97e788fe12
Siren: c0f6012f61196b73455202db07730f6454a4beb0
sqlite3: f163dbbb7aa3339ad8fc622782c2d9d7b72f7e9c
-PODFILE CHECKSUM: 1200ca7a56742e3cfaae6fc3c01a9f5035849f4b
+PODFILE CHECKSUM: f84de3dc2812d0990dad2f2ba141bcad0ba9bfcd
COCOAPODS: 1.16.2
diff --git a/iosApp/iosApp/engine/IosOonimkallBridge.swift b/iosApp/iosApp/engine/IosOonimkallBridge.swift
index 83fc6c5da..2e9f702df 100644
--- a/iosApp/iosApp/engine/IosOonimkallBridge.swift
+++ b/iosApp/iosApp/engine/IosOonimkallBridge.swift
@@ -147,6 +147,9 @@ extension OonimkallBridgeSessionConfig {
config.stateDir = stateDir
config.tempDir = tempDir
config.tunnelDir = tunnelDir
+ if let geoIpDB = geoIpDB {
+ config.geoipDB = geoIpDB
+ }
if let probeServicesURL = probeServicesURL {
config.probeServicesURL = probeServicesURL
}