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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ composeApp/src/desktopMain/resources/windows/WinSparkle.*

/composeApp/src/desktopMain/frameworks/
composeApp/macos-appcast.xml
*.mmdb

certificates/*.pem
4 changes: 2 additions & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.ooni.probe.net

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.ooni.engine.models.Failure
import org.ooni.engine.models.Result
import org.ooni.engine.models.Success
import org.ooni.probe.data.models.GetBytesException
import java.net.HttpURLConnection
import java.net.URL

actual suspend fun httpGetBytes(url: String): Result<ByteArray, GetBytesException> =
withContext(Dispatchers.IO) {
val connection: HttpURLConnection
try {
connection = URL(url).openConnection() as HttpURLConnection
} catch (e: Throwable) {
return@withContext Failure(GetBytesException(e))
}

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) {
Failure(GetBytesException(RuntimeException("HTTP $code while GET $url: ${String(bytes)}")))
} else {
Success(bytes)
}
} catch (e: Throwable) {
Failure(GetBytesException(e))
} finally {
connection.disconnect()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
<string name="r2160p_ext" translatable="false">2160p (4k)</string>

<string name="twoParam" translatable="false">%1$s %2$s</string>
<string name="engine_mmdb_version" translatable="false">20250801</string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's easier if this is a regular constant, instead of a string resource? We're not taking advantage of the resource system for anything here.

</resources>
4 changes: 3 additions & 1 deletion composeApp/src/commonMain/kotlin/org/ooni/engine/Engine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class Engine(

private fun session(sessionConfig: OonimkallBridge.SessionConfig): OonimkallBridge.Session = bridge.newSession(sessionConfig)

private fun buildTaskSettings(
private suspend fun buildTaskSettings(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspend should not be needed.

netTest: NetTest,
taskOrigin: TaskOrigin,
preferences: EnginePreferences,
Expand All @@ -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),
Expand Down Expand Up @@ -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,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Logger.d("GeoIP DB info in resolver lookup: $it")
null
}

"status.measurement_done" ->
TaskEvent.MeasurementDone(index = value?.idx ?: 0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ data class EnginePreferences(
val taskLogLevel: TaskLogLevel,
val uploadResults: Boolean,
val proxy: String?,
val geoipDbVersion: String?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we also need to tell the engine what version of the Db?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more to determine the path to themmdb file if it exists.

val maxRuntime: Duration?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ sealed interface TaskEvent {
val ip: String?,
val asn: String?,
val countryCode: String?,
val geoIpdb: String?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to get the geoIpDb from the engine?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To determine which version of geoIPDB the engine is currently working with.

val networkType: NetworkType,
) : TaskEvent

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions composeApp/src/commonMain/kotlin/org/ooni/probe/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ fun App(
// ForegroundServiceDidNotStartInTimeException some users are getting
// dependencies.startSingleRunInner(RunSpecification.OnlyUploadMissingResults)
}
LaunchedEffect(Unit) {
dependencies.fetchGeoIpDbUpdates()
}
LaunchedEffect(Unit) {
dependencies.observeAndConfigureAutoUpdate()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.ooni.probe.data.models

class GetBytesException(
t: Throwable,
) : Exception(t)
Original file line number Diff line number Diff line change
Expand Up @@ -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"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -80,6 +81,10 @@ class PreferenceRepository(
SettingsKey.DELETE_OLD_RESULTS_THRESHOLD,
-> PreferenceKey.IntKey(intPreferencesKey(preferenceKey))

SettingsKey.MMDB_LAST_CHECK,
-> PreferenceKey.LongKey(longPreferencesKey(preferenceKey))

SettingsKey.MMDB_VERSION,
SettingsKey.LEGACY_PROXY_HOSTNAME,
SettingsKey.LEGACY_PROXY_PROTOCOL,
SettingsKey.PROXY_SELECTED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = ::httpGetBytes,
)
}

val fetchGeoIpDbUpdates by lazy {
FetchGeoIpDbUpdates(
downloadFile = downloader::invoke,
cacheDir = cacheDir,
engineHttpDo = engine::httpDo,
json = json,
preferencesRepository = preferenceRepository,
)
}

@VisibleForTesting
val engine by lazy {
Engine(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.ooni.probe.domain

import okio.FileSystem
import okio.Path
import okio.Path.Companion.toPath
import okio.buffer
import okio.use
import org.ooni.engine.models.Result
import org.ooni.probe.data.models.GetBytesException

/**
* 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) -> Result<ByteArray, GetBytesException>,
) {
suspend operator fun invoke(
url: String,
absoluteTargetPath: String,
): Result<Path, GetBytesException> {
val target = absoluteTargetPath.toPath()
target.parent?.let { parent ->
if (fileSystem.metadataOrNull(parent) == null) fileSystem.createDirectories(parent)
}
return fetchBytes(url).map { bytes ->
fileSystem.sink(target).buffer().use { sink -> sink.write(bytes) }
target
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.ooni.probe.domain

import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.first
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
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.Engine.MkException
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.GetBytesException
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) -> Result<Path, GetBytesException>,
private val cacheDir: String,
private val engineHttpDo: suspend (method: String, url: String, taskOrigin: TaskOrigin) -> Result<String?, Engine.MkException>,
private val preferencesRepository: PreferenceRepository,
private val json: Json,
) {
suspend operator fun invoke(): Result<Path?, Engine.MkException> =
getLatestEngineVersion()
.onSuccess { version ->
val (isLatest, _, latestVersion) = isGeoIpDbLatest(version)
if (isLatest) {
return Success(null)
} else {
val versionName = latestVersion
val url = buildGeoIpDbUrl(versionName)
val target = "$cacheDir/$versionName.mmdb"

downloadFile(url, target)
.onSuccess { downloadedPath ->
preferencesRepository.setValueByKey(
SettingsKey.MMDB_VERSION,
versionName,
)
preferencesRepository.setValueByKey(
SettingsKey.MMDB_LAST_CHECK,
Clock.System.now().toEpochMilliseconds(),
)
return Success(downloadedPath)
}.onFailure { downloadError ->
return Failure(Engine.MkException(downloadError))
}
}
}.onFailure { versionError ->
return Failure(versionError)
}.let { Failure(Engine.MkException(Throwable("Unexpected state"))) }

/**
* Compare latest and current version integers and return pair of latest state and actual version number
* @return Triple<Boolean, String, String> 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<Boolean, String, String> {
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<String, MkException> {
val url = "https://api.github.com/repos/aanorbel/oomplt-mmdb/releases/latest"

return engineHttpDo("GET", url, TaskOrigin.OoniRun).map { payload ->
payload?.let {
try {
json.decodeFromString(GhRelease.serializer(), payload).tag
} catch (e: SerializationException) {
Logger.e(e) { "Failed to decode release info" }
null
} catch (e: IllegalArgumentException) {
Logger.e(e) { "Failed to decode release info" }
null
}
} ?: throw MkException(Throwable("Failed to fetch latest version"))
}
}

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,
)
}
Loading
Loading