From 3a1735eb3a5082782f19288eeb97411fd979c11c Mon Sep 17 00:00:00 2001 From: programminghoch10 <16062290+programminghoch10@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:45:16 +0100 Subject: [PATCH 1/2] implement server side log submissions --- api/server/src/main/kotlin/ApiServer.kt | 8 ++ api/server/src/main/kotlin/routes/api/base.kt | 14 +++ .../src/main/kotlin/routes/stats/base.kt | 1 + .../src/main/kotlin/routes/stats/log.kt | 88 +++++++++++++++++++ .../main/resources/templates/home.mustache | 1 + .../main/resources/templates/logs.mustache | 40 +++++++++ api/src/main/kotlin/Api.kt | 6 ++ api/src/main/kotlin/json/LOG.kt | 15 ++++ 8 files changed, 173 insertions(+) create mode 100644 api/server/src/main/kotlin/routes/stats/log.kt create mode 100644 api/server/src/main/resources/templates/logs.mustache create mode 100644 api/src/main/kotlin/json/LOG.kt diff --git a/api/server/src/main/kotlin/ApiServer.kt b/api/server/src/main/kotlin/ApiServer.kt index b653561..b9f9326 100644 --- a/api/server/src/main/kotlin/ApiServer.kt +++ b/api/server/src/main/kotlin/ApiServer.kt @@ -14,6 +14,7 @@ import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import de.binarynoise.captiveportalautologin.api.Api +import de.binarynoise.captiveportalautologin.api.json.LOG import de.binarynoise.captiveportalautologin.api.json.har.HAR import de.binarynoise.filedb.JsonDB import de.binarynoise.logger.Logger.log @@ -68,6 +69,13 @@ class ApiServer(root: Path = Path(".")) : Api { } } + override val log: Api.Log = object : Api.Log { + override fun submitLog(name: String, log: LOG) { + jsonDb.store(name, log, "log") + log("stored log $name") + } + } + override val liberator: Api.Liberator = object : Api.Liberator { override fun getLiberatorVersion(): String { TODO("getLiberatorVersion Not yet implemented") diff --git a/api/server/src/main/kotlin/routes/api/base.kt b/api/server/src/main/kotlin/routes/api/base.kt index 96100c9..366e3fa 100644 --- a/api/server/src/main/kotlin/routes/api/base.kt +++ b/api/server/src/main/kotlin/routes/api/base.kt @@ -1,6 +1,10 @@ package de.binarynoise.captiveportalautologin.server.routes.api +import kotlin.time.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import de.binarynoise.captiveportalautologin.api.Api +import de.binarynoise.captiveportalautologin.api.json.LOG import de.binarynoise.captiveportalautologin.api.json.har.HAR import de.binarynoise.captiveportalautologin.server.ApiServer import de.binarynoise.captiveportalautologin.server.routes.missingParameter @@ -9,6 +13,8 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +private fun dateTime() = Clock.System.now().toLocalDateTime(TimeZone.UTC) + fun Routing.api() { route("/api") { get("/") { @@ -22,6 +28,14 @@ fun Routing.api() { call.respond(HttpStatusCode.Created) } } + route("/log") { + put("/{name}") { + val file = call.receive() + val name = dateTime().toString() + ApiServer.api.log.submitLog(name, file) + call.respond(HttpStatusCode.Created) + } + } route("/liberator") { put("error") { it: Api.Liberator.Error -> ApiServer.api.liberator.reportError(it) diff --git a/api/server/src/main/kotlin/routes/stats/base.kt b/api/server/src/main/kotlin/routes/stats/base.kt index cf0a6e5..26455d9 100644 --- a/api/server/src/main/kotlin/routes/stats/base.kt +++ b/api/server/src/main/kotlin/routes/stats/base.kt @@ -24,5 +24,6 @@ fun Routing.stats() { successRoutes() errorRoutes() harRoutes() + logRoutes() } } diff --git a/api/server/src/main/kotlin/routes/stats/log.kt b/api/server/src/main/kotlin/routes/stats/log.kt new file mode 100644 index 0000000..a583c07 --- /dev/null +++ b/api/server/src/main/kotlin/routes/stats/log.kt @@ -0,0 +1,88 @@ +package de.binarynoise.captiveportalautologin.server.routes.stats + +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.moveTo +import de.binarynoise.captiveportalautologin.api.json.LOG +import de.binarynoise.captiveportalautologin.server.ApiServer +import de.binarynoise.logger.Logger +import io.ktor.http.* +import io.ktor.server.mustache.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +internal fun Route.logRoutes() { + get("log") { + call.response.header("Location", "log/") + call.respond(HttpStatusCode.MovedPermanently) + } + + route("log/") { + get { + val names = ApiServer.api.jsonDb.listAll("log") + val entries = names.map { name -> + val log = ApiServer.api.jsonDb.load(name, "log") + val timestamp = log.timestamp + val version = log.version + mapOf( + "name" to name, + "timestamp" to timestamp, + "version" to version, + "view" to "view/$name.log", + "download" to "download/$name.log", + "archiveAction" to "archive/$name.log", + ) + }.sortedByDescending { it["timestamp"] as String } + + call.respond( + MustacheContent( + "logs.mustache", mapOf( + "title" to "Log Files", + "backLink" to "../", + "entries" to entries, + ) + ) + ) + } + + fun downloadRoutingHandler(inline: Boolean = false): RoutingHandler = get@{ + val id = call.parameters["id"] ?: error("id not set") + + val path = ApiServer.api.jsonDb.base().resolve(id) + if (!path.exists()) { + Logger.log("file not found: $path") + call.respond(HttpStatusCode.NotFound) + return@get + } + val log = ApiServer.api.jsonDb.load(id.removeSuffix(".log"), "log") + val contentDispositionBase = if (inline) ContentDisposition.Inline else ContentDisposition.Attachment + call.response.header( + HttpHeaders.ContentDisposition, + contentDispositionBase.withParameter(ContentDisposition.Parameters.FileName, log.name).toString(), + ) + call.respondText(log.content) + } + get("download/{id}", downloadRoutingHandler()) + get("view/{id}", downloadRoutingHandler(true)) + + post("archive/{id}") { + val id = call.parameters["id"] ?: error("id not set") + val base = ApiServer.api.jsonDb.base() + val src = base.resolve(id) + if (!src.exists()) { + Logger.log("file not found: $src") + call.respond(HttpStatusCode.NotFound) + return@post + } + val archiveDir = base.resolve("archived").apply { createDirectories() } + var dest = archiveDir.resolve(id) + if (dest.exists()) { + val alt = "$id-archived-${System.currentTimeMillis()}" + dest = archiveDir.resolve(alt) + } + src.moveTo(dest) + call.response.header("Location", "../") + call.respond(HttpStatusCode.SeeOther) + } + } +} diff --git a/api/server/src/main/resources/templates/home.mustache b/api/server/src/main/resources/templates/home.mustache index 54b0168..8858b2a 100644 --- a/api/server/src/main/resources/templates/home.mustache +++ b/api/server/src/main/resources/templates/home.mustache @@ -3,5 +3,6 @@

Successes

Errors

Submitted HAR files

+

Submitted log files

{{/content}} {{/base-layout}} diff --git a/api/server/src/main/resources/templates/logs.mustache b/api/server/src/main/resources/templates/logs.mustache new file mode 100644 index 0000000..f7d8f98 --- /dev/null +++ b/api/server/src/main/resources/templates/logs.mustache @@ -0,0 +1,40 @@ +{{ + + + Name + Timestamp + Version + View + Download + Archive + + + + {{#entries}} + + {{name}} + {{timestamp}} + {{version}} + + + + + + + + + + + +
+ +
+ + + {{/entries}} + + + {{/content}} +{{/base-layout}} diff --git a/api/src/main/kotlin/Api.kt b/api/src/main/kotlin/Api.kt index 64c9271..b4007a4 100644 --- a/api/src/main/kotlin/Api.kt +++ b/api/src/main/kotlin/Api.kt @@ -1,16 +1,22 @@ package de.binarynoise.captiveportalautologin.api import kotlinx.serialization.Serializable +import de.binarynoise.captiveportalautologin.api.json.LOG import de.binarynoise.captiveportalautologin.api.json.har.HAR interface Api { val har: Har + val log: Log val liberator: Liberator interface Har { fun submitHar(name: String, har: HAR) } + interface Log { + fun submitLog(name: String, log: LOG) + } + interface Liberator { fun getLiberatorVersion(): String fun fetchLiberatorUpdate() diff --git a/api/src/main/kotlin/json/LOG.kt b/api/src/main/kotlin/json/LOG.kt new file mode 100644 index 0000000..581c309 --- /dev/null +++ b/api/src/main/kotlin/json/LOG.kt @@ -0,0 +1,15 @@ +package de.binarynoise.captiveportalautologin.api.json + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * + */ +@Serializable +data class LOG( + @SerialName("name") var name: String, + @SerialName("timestamp") var timestamp: String, + @SerialName("version") var version: String, + @SerialName("content") var content: String, +) From 91017e86deabdf68f5d2d615c9acb51726a85b3f Mon Sep 17 00:00:00 2001 From: programminghoch10 <16062290+programminghoch10@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:26:36 +0100 Subject: [PATCH 2/2] implement client side log submissions --- api/client/src/main/kotlin/ApiClient.kt | 9 +++++ .../captiveportalautologin/Stats.kt | 17 ++++++++- .../preferences/LogsFragment.kt | 38 +++++++++++++++++++ app/src/main/res/layout/item_log_export.xml | 11 +++++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/api/client/src/main/kotlin/ApiClient.kt b/api/client/src/main/kotlin/ApiClient.kt index 1d52ad4..69703e2 100644 --- a/api/client/src/main/kotlin/ApiClient.kt +++ b/api/client/src/main/kotlin/ApiClient.kt @@ -2,6 +2,7 @@ package de.binarynoise.captiveportalautologin.client import kotlinx.serialization.json.Json import de.binarynoise.captiveportalautologin.api.Api +import de.binarynoise.captiveportalautologin.api.json.LOG import de.binarynoise.captiveportalautologin.api.json.har.HAR import de.binarynoise.util.okhttp.checkSuccess import de.binarynoise.util.okhttp.postJson @@ -17,6 +18,13 @@ class ApiClient(private val base: HttpUrl) : Api { put("har/$name", har.toJson()) } } + + override val log = object : Api.Log { + override fun submitLog(name: String, log: LOG) { + put("log/$name", log.toJson()) + } + } + override val liberator = object : Api.Liberator { override fun getLiberatorVersion(): String { TODO("Not yet implemented") @@ -45,6 +53,7 @@ class ApiClient(private val base: HttpUrl) : Api { } fun HAR.toJson(): String = serializer.encodeToString(this) +fun LOG.toJson(): String = serializer.encodeToString(this) val serializer = Json { encodeDefaults = false diff --git a/app/src/main/kotlin/de/binarynoise/captiveportalautologin/Stats.kt b/app/src/main/kotlin/de/binarynoise/captiveportalautologin/Stats.kt index 4686d53..a730c66 100644 --- a/app/src/main/kotlin/de/binarynoise/captiveportalautologin/Stats.kt +++ b/app/src/main/kotlin/de/binarynoise/captiveportalautologin/Stats.kt @@ -17,6 +17,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters import de.binarynoise.captiveportalautologin.api.Api +import de.binarynoise.captiveportalautologin.api.json.LOG import de.binarynoise.captiveportalautologin.api.json.har.HAR import de.binarynoise.captiveportalautologin.client.ApiClient import de.binarynoise.captiveportalautologin.preferences.SharedPreferences @@ -67,6 +68,12 @@ class StatsWorker(appContext: Context, workerParams: WorkerParameters) : Corouti jsonDB.delete(key, "har") log("Uploaded HAR $key") } + "log" -> { + val log = jsonDB.load(key, "log") + apiClient.log.submitLog(key, log) + jsonDB.delete(key, "log") + log("Uploaded log $key") + } "error" -> { val error = jsonDB.load(key) apiClient.liberator.reportError(error) @@ -116,9 +123,9 @@ class StatsWorker(appContext: Context, workerParams: WorkerParameters) : Corouti object Stats : Api { override val har: Har = Har() + override val log: Log = Log() override val liberator: Liberator = Liberator() - class Har : Api.Har { override fun submitHar(name: String, har: HAR) { val key = name @@ -127,6 +134,14 @@ object Stats : Api { } } + class Log : Api.Log { + override fun submitLog(name: String, log: LOG) { + val key = name + jsonDB.store(key, log, "log") + scheduleUpload("log", key) + } + } + class Liberator : Api.Liberator { override fun getLiberatorVersion(): String { TODO("Not yet implemented") diff --git a/app/src/main/kotlin/de/binarynoise/captiveportalautologin/preferences/LogsFragment.kt b/app/src/main/kotlin/de/binarynoise/captiveportalautologin/preferences/LogsFragment.kt index d4dd885..03053ea 100644 --- a/app/src/main/kotlin/de/binarynoise/captiveportalautologin/preferences/LogsFragment.kt +++ b/app/src/main/kotlin/de/binarynoise/captiveportalautologin/preferences/LogsFragment.kt @@ -10,7 +10,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceCategory +import de.binarynoise.captiveportalautologin.BuildConfig import de.binarynoise.captiveportalautologin.R +import de.binarynoise.captiveportalautologin.Stats +import de.binarynoise.captiveportalautologin.api.json.LOG import de.binarynoise.captiveportalautologin.databinding.ItemLogExportBinding import de.binarynoise.captiveportalautologin.util.FileUtils import de.binarynoise.captiveportalautologin.util.FileUtils.shareFile @@ -74,9 +77,44 @@ class LogsFragment : AutoCleanupPreferenceFragment() { } } } + uploadButton.setOnClickListener { + lifecycleScope.launch { + try { + val toast = Toast.makeText( + view.context, "Preparing upload...", Toast.LENGTH_SHORT + ) + toast.show() + + withContext(Dispatchers.IO) { + val timestamp = file.name.removeSuffix(".log") + val version = BuildConfig.VERSION_NAME + val name = "$timestamp $version" + val log = LOG( + name, + timestamp, + version, + file.readText(), + ) + Stats.log.submitLog(name, log) + } + + toast.cancel() + Toast.makeText(view.context, "Upload scheduled", Toast.LENGTH_SHORT) + .show() + } catch (e: Exception) { + Toast.makeText( + view.context, + e::class.java.simpleName + ": " + e.message + "\n" + "Please try again.", + Toast.LENGTH_SHORT, + ).show() + log("Error scheduling upload", e) + } + } + } } }) { title = file.name + isIconSpaceReserved = false } } } diff --git a/app/src/main/res/layout/item_log_export.xml b/app/src/main/res/layout/item_log_export.xml index 7ec9830..bede250 100644 --- a/app/src/main/res/layout/item_log_export.xml +++ b/app/src/main/res/layout/item_log_export.xml @@ -28,11 +28,20 @@