Skip to content
Open
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
9 changes: 9 additions & 0 deletions api/client/src/main/kotlin/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions api/server/src/main/kotlin/ApiServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
14 changes: 14 additions & 0 deletions api/server/src/main/kotlin/routes/api/base.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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("/") {
Expand All @@ -22,6 +28,14 @@ fun Routing.api() {
call.respond(HttpStatusCode.Created)
}
}
route("/log") {
put("/{name}") {
val file = call.receive<LOG>()
val name = dateTime().toString()
ApiServer.api.log.submitLog(name, file)
call.respond(HttpStatusCode.Created)
}
}
route("/liberator") {
put<Api.Liberator.Error>("error") { it: Api.Liberator.Error ->
ApiServer.api.liberator.reportError(it)
Expand Down
1 change: 1 addition & 0 deletions api/server/src/main/kotlin/routes/stats/base.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ fun Routing.stats() {
successRoutes()
errorRoutes()
harRoutes()
logRoutes()
}
}
88 changes: 88 additions & 0 deletions api/server/src/main/kotlin/routes/stats/log.kt
Original file line number Diff line number Diff line change
@@ -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>("log")
val entries = names.map { name ->
val log = ApiServer.api.jsonDb.load<LOG>(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<LOG>().resolve(id)
if (!path.exists()) {
Logger.log("file not found: $path")
call.respond(HttpStatusCode.NotFound)
return@get
}
val log = ApiServer.api.jsonDb.load<LOG>(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<LOG>()
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)
}
}
}
1 change: 1 addition & 0 deletions api/server/src/main/resources/templates/home.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<p><a href="successes/">Successes</a></p>
<p><a href="errors/">Errors</a></p>
<p><a href="har/">Submitted HAR files</a></p>
<p><a href="log/">Submitted log files</a></p>
{{/content}}
{{/base-layout}}
40 changes: 40 additions & 0 deletions api/server/src/main/resources/templates/logs.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{{<base-layout}}
{{$content}}
<table>
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Version</th>
<th>View</th>
<th>Download</th>
<th>Archive</th>
</tr>
</thead>
<tbody>
{{#entries}}
<tr class="mono">
<td>{{name}}</td>
<td>{{timestamp}}</td>
<td>{{version}}</td>
<td>
<a href="{{view}}">
<button type="button">View</button>
</a>
</td>
<td>
<a href="{{download}}">
<button type="button">Download</button>
</a>
</td>
<td>
<form action="{{archiveAction}}" method="post">
<button type="submit">Archive</button>
</form>
</td>
</tr>
{{/entries}}
</tbody>
</table>
{{/content}}
{{/base-layout}}
6 changes: 6 additions & 0 deletions api/src/main/kotlin/Api.kt
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
15 changes: 15 additions & 0 deletions api/src/main/kotlin/json/LOG.kt
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -67,6 +68,12 @@ class StatsWorker(appContext: Context, workerParams: WorkerParameters) : Corouti
jsonDB.delete<HAR>(key, "har")
log("Uploaded HAR $key")
}
"log" -> {
val log = jsonDB.load<LOG>(key, "log")
apiClient.log.submitLog(key, log)
jsonDB.delete<LOG>(key, "log")
log("Uploaded log $key")
}
"error" -> {
val error = jsonDB.load<Api.Liberator.Error>(key)
apiClient.liberator.reportError(error)
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/res/layout/item_log_export.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,20 @@

<Button
android:id="@+id/copyToSdButton"
style="@style/CaptivePortalAutoLogin.Button.Accent"
style="@style/CaptivePortalAutoLogin.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="copy to SD"
android:textAllCaps="false"
/>

<Button
android:id="@+id/uploadButton"
style="@style/CaptivePortalAutoLogin.Button.Accent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="upload"
android:textAllCaps="false"
/>

</LinearLayout>
Loading