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 }