-
Notifications
You must be signed in to change notification settings - Fork 6
chore: draft for using geoipdb from path
#940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)}") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| bytes | ||
| } finally { | ||
| connection.disconnect() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -148,7 +148,7 @@ class Engine( | |
|
|
||
| private fun session(sessionConfig: OonimkallBridge.SessionConfig): OonimkallBridge.Session = bridge.newSession(sessionConfig) | ||
|
|
||
| private fun buildTaskSettings( | ||
| private suspend fun buildTaskSettings( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we need to keep this, then we should use Logger. |
||
| null | ||
| } | ||
|
|
||
| "status.measurement_done" -> | ||
| TaskEvent.MeasurementDone(index = value?.idx ?: 0) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,5 +7,6 @@ data class EnginePreferences( | |
| val taskLogLevel: TaskLogLevel, | ||
| val uploadResults: Boolean, | ||
| val proxy: String?, | ||
| val geoipDbVersion: String?, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| val maxRuntime: Duration?, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ sealed interface TaskEvent { | |
| val ip: String?, | ||
| val asn: String?, | ||
| val countryCode: String?, | ||
| val geoIpdb: String?, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to get the geoIpDb from the engine? |
||
| val networkType: NetworkType, | ||
| ) : TaskEvent | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need |
||
| } | ||
| LaunchedEffect(Unit) { | ||
| dependencies.observeAndConfigureAutoUpdate() | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as |
||
| ) | ||
| } | ||
|
|
||
| val fetchGeoIpDbUpdates by lazy { | ||
| FetchGeoIpDbUpdates( | ||
| downloadFile = downloader::invoke, | ||
| cacheDir = cacheDir, | ||
| engineHttpDo = engine::httpDo, | ||
| json = json, | ||
| preferencesRepository = preferenceRepository, | ||
| ) | ||
| } | ||
|
|
||
| @VisibleForTesting | ||
| val engine by lazy { | ||
| Engine( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no error handling here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also be receiving a stream from the HTTP call, and be storing it straight away in a file, instead of keeping it all in memory. For a 15MB file it's not a huge deal, but it would be the optimal way. |
||
| val existing = fileSystem.metadataOrNull(target) | ||
| if (existing?.size == bytes.size.toLong()) return target | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not store if it's the same size? Couldn't it be a newer version, but with the same size? |
||
| fileSystem.sink(target).buffer().use { sink -> sink.write(bytes) } | ||
| return target | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<String?, Engine.MkException>, | ||
| private val preferencesRepository: PreferenceRepository, | ||
| private val json: Json, | ||
| ) { | ||
| suspend operator fun invoke(): Result<Path?, Engine.MkException> = | ||
| try { | ||
| when (val versionRes = getLatestEngineVersion()) { | ||
| is Failure -> Failure(versionRes.reason) | ||
| is Success -> { | ||
| val (isLatest, _, latestVersion) = isGeoIpDbLatest(versionRes.value) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're ignoring the currentVersion why return it? |
||
| if (isLatest) { | ||
| Success(null) | ||
| } else { | ||
| val versionName = latestVersion | ||
| val url = buildGeoIpDbUrl(versionName) | ||
| val target = "$cacheDir/$versionName.mmdb" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally you should be using the Okio |
||
|
|
||
| downloadFile(url, target).let { | ||
| preferencesRepository.setValueByKey(SettingsKey.MMDB_VERSION, versionName) | ||
| preferencesRepository.setValueByKey(SettingsKey.MMDB_LAST_CHECK, Clock.System.now().toEpochMilliseconds()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're storing this but never using it? Should we be checking this before even checking for new releases? |
||
| 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<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, Engine.MkException> { | ||
| 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")) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Throwing from inside a |
||
| 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, | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ class GetEnginePreferences( | |
| null | ||
| }, | ||
| proxy = getProxyOption().first().value, | ||
| geoipDbVersion = getValueForKey(SettingsKey.MMDB_VERSION) as String?, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should already be providing the DB path here, instead of having to construct it in multiple places. |
||
| ) | ||
|
|
||
| private suspend fun getEnabledCategories(): List<String> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You didn't trust the engine httpDo to fetch a larger file? And you preferred not to use a library like ktor? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I think this should return a |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No desktop support from the start? |
||
| } | ||
| it.stateDir = stateDir | ||
| it.tempDir = tempDir | ||
| it.tunnelDir = tunnelDir | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
openConnection()may also throw an IOException