Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c08b086
improve routing
binarynoise Apr 30, 2026
61209b0
improve logging
binarynoise Apr 30, 2026
d17e7ab
improve backLink
binarynoise Apr 30, 2026
586cd57
add Decoroutinator
binarynoise Apr 30, 2026
7588f53
make ApiServer Port and Host configurable with environment variables
binarynoise Apr 30, 2026
69ad6ef
implement interactive tables for querying successes
binarynoise Apr 30, 2026
6192514
update shadowJar configuration
binarynoise May 4, 2026
500d5fe
enable hot reload only when not running from jar
binarynoise May 4, 2026
5e202fe
return proper 500 for exceptions
binarynoise Apr 30, 2026
fd2953a
downgrade ktor to fix hot-reload
binarynoise May 9, 2026
97319b5
fix pre-filter
binarynoise May 9, 2026
e1fd3d9
more noise
binarynoise May 9, 2026
7d0d125
limit width of table cells
binarynoise May 9, 2026
2790202
make filter input field use all available space
binarynoise May 9, 2026
a031642
implement interactive tables for querying errors
binarynoise May 9, 2026
9ea72f9
remove unneeded experimental opt-ins
binarynoise May 10, 2026
220c7d2
disable incremental ksp to fix compilation when two gradles run in pa…
binarynoise May 12, 2026
db9bd8d
print DB base path on startup
binarynoise May 12, 2026
68c77d5
implement interactive tables for querying HARs
binarynoise May 12, 2026
1fabe1c
unify naming
binarynoise May 12, 2026
65bdc1b
improve Comparators
binarynoise May 12, 2026
35aff35
add defaultSort option
binarynoise May 12, 2026
d4414a5
extract common functionality
binarynoise May 13, 2026
7852c68
add proper icons for sorting
binarynoise May 14, 2026
a09ef85
fix color scheme css
binarynoise May 14, 2026
b2104a0
move input and label closer together
binarynoise May 14, 2026
a0b960e
keep indents on empty lines for CSS like files
binarynoise May 14, 2026
7d821d9
retain preFilter, set some default preFilters
binarynoise May 20, 2026
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
20 changes: 20 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions api/server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
application
alias(libs.plugins.buildlogic.kotlin.jvm)
alias(libs.plugins.kotlin.dataframe)
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.androidx.room)
alias(libs.plugins.shadow)
Expand All @@ -14,6 +15,8 @@ dependencies {
implementation(projects.util.fileDB)
implementation(projects.util.logger)

implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.dataframe)
implementation(libs.decoroutinator)

implementation(platform(libs.ktor.bom))
Expand Down Expand Up @@ -62,14 +65,25 @@ tasks.withType<ShadowJar> {
minimize {
exclude(dependency(libs.ktor.serialization.kotlinx.json.get()))
exclude(dependency(libs.slf4j.simple.get()))
exclude(dependency(libs.decoroutinator.get()))
exclude(dependency(libs.kotlin.reflect.get()))
}
exclude(
"**/*.kotlin_*",
"**/*.pro",
"/*.css",
"/*.html",
"/*.js",
"/*/default/linkdata/",
"/*/default/manifest",
"/DebugProbesKt.bin",
"/META-INF/**/*.kotlin_*",
"/META-INF/**/*.pro",
"/META-INF/**/*.version*",
"/META-INF/**/*LICENSE*",
"/META-INF/**/*NOTICE*",
"/META-INF/*jupyter*/",
"/META-INF/maven/",
"/META-INF/native-image/",
"/META-INF/maven/"
"/META-INF/versions/",
"/org/apache/commons/codec/language/**/*.txt"
)
}
6 changes: 1 addition & 5 deletions api/server/src/main/kotlin/ApiServer.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
@file:OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)

package de.binarynoise.captiveportalautologin.server

import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
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.har.HAR
Expand All @@ -38,7 +34,7 @@ class ApiServer(root: Path = Path(".")) : Api {
runBlocking {
database.useConnection(isReadOnly = true) {}
}
log("Database initialized")
log("Database initialized at root ${root.toAbsolutePath()}")
}

override val har: Api.Har = object : Api.Har {
Expand Down
11 changes: 8 additions & 3 deletions api/server/src/main/kotlin/LoggingPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ val LoggingPlugin: ApplicationPlugin<Unit> = createApplicationPlugin(name = "Log
append(call.request.httpMethod.value)
append(" ")
append(call.request.origin.uri)
append(" with body ")
append(" with status code ")
append(call.response.status()?.value ?: 200)
append(" and body '")
append(body.toString().substringBefore("\n").take(100))
append("'")
})
}

on(CallFailed, handler = object : suspend (ApplicationCall, Throwable) -> Unit {
override suspend fun invoke(call: ApplicationCall, cause: Throwable) {
if (cause is CancellationException) throw cause
Logger.log("call failed", cause)
when (cause) {
is CancellationException -> throw cause
else -> Logger.log("call failed", cause)
}
}
})

Expand Down
29 changes: 17 additions & 12 deletions api/server/src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
@file:OptIn(ExperimentalSerializationApi::class)

package de.binarynoise.captiveportalautologin.server

import kotlin.io.path.Path
import kotlin.io.path.exists
import kotlin.io.path.readText
import kotlin.reflect.jvm.javaMethod
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import com.github.mustachejava.DefaultMustacheFactory
import com.github.mustachejava.FragmentKey
import com.github.mustachejava.Mustache
import de.binarynoise.captiveportalautologin.server.ApiServer.Companion.api
import de.binarynoise.captiveportalautologin.server.routes.configureRouting
import de.binarynoise.logger.Logger.log
import dev.reformator.stacktracedecoroutinator.jvm.DecoroutinatorJvmApi
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
Expand All @@ -27,9 +26,15 @@ import io.ktor.server.response.*

val hostname = Path("/proc/sys/kernel/hostname").takeIf { it.exists() }?.readText()?.trim()
val isDevelopment = hostname != "captiveportalautologin"
val isRunningFromJar = ::main.javaMethod!!.declaringClass.protectionDomain.codeSource.location.path.endsWith(".jar")

fun main() {
val server = createServer("::", 8080)
if (isDevelopment) DecoroutinatorJvmApi.install()

val port = System.getenv("API_SERVER_PORT")?.toInt() ?: 8080
val host = System.getenv("API_SERVER_HOST") ?: "::"
log("launching server at $host:$port")
val server = createServer(host, port)
server.start(wait = true)
}

Expand All @@ -39,18 +44,18 @@ fun createServer(host: String, port: Int): EmbeddedServer<*, *> {
factory = Netty,
port = port,
host = host,
watchPaths = listOf("classes", "resources"),
watchPaths = if (isRunningFromJar) emptyList() else listOf("classes", "resources"),
module = Application::module,
)
with(server.engineConfig) {
shutdownTimeout = 1000
enableHttp2 = false
enableH2c = false
// enableH2c = false
}
return server
}

suspend fun Application.module() {
/*suspend*/ fun Application.module() { // TODO: make this suspend again for ktor >=3.2.0
api = ApiServer(Path(System.getenv("API_SERVER_PATH") ?: "."))

check(developmentMode == isDevelopment) { "developmentMode != isDevelopment" }
Expand Down Expand Up @@ -83,11 +88,11 @@ suspend fun Application.module() {
})
}
install(StatusPages) {
exception<CancellationException> { call, cause ->
throw cause
}
exception<IllegalArgumentException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, cause.message ?: "Illegal Arguments")
exception<Throwable> { call, cause ->
when (cause) {
is CancellationException -> throw cause
else -> call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
}
unhandled { call ->
System.err.println("unhandled call: ${call.request.httpMethod.value} ${call.request.uri}")
Expand Down
31 changes: 6 additions & 25 deletions api/server/src/main/kotlin/database/ErrorDao.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package de.binarynoise.captiveportalautologin.server.database

import kotlin.time.Instant
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
Expand All @@ -10,11 +9,11 @@ interface ErrorDao {
@Insert
suspend fun insert(error: ErrorEntity)

@Query("SELECT * FROM errors ORDER BY timestamp DESC LIMIT :limit")
suspend fun getAllErrors(limit: Int): List<ErrorEntity>
@Query("SELECT * FROM errors ORDER BY timestamp DESC")
suspend fun getAll(): List<ErrorEntity>

@Query("SELECT * FROM errors WHERE message LIKE 'unknown portal' ORDER BY timestamp DESC LIMIT :limit")
suspend fun getUnknownPortalErrors(limit: Int): List<ErrorEntity>
@Query("SELECT * FROM errors WHERE message LIKE 'unknown portal' ORDER BY timestamp DESC")
suspend fun getUnknownPortals(): List<ErrorEntity>

@Query(
"""
Expand All @@ -27,27 +26,9 @@ interface ErrorDao {
AND message NOT LIKE 'Binding socket to network % failed: %'
AND message NOT LIKE 'Chain validation failed'
AND message NOT LIKE 'java.security.cert.CertPathValidatorException: %'
AND message NOT LIKE 'Socket is closed'
ORDER BY timestamp DESC
LIMIT :limit
"""
)
suspend fun getNoNoiseErrors(limit: Int): List<ErrorEntity>

@Query(
"""
SELECT * FROM errors
WHERE timestamp BETWEEN :start AND :end
AND version = :version
AND message = :message
AND url LIKE '%' || :domain || '%'
ORDER BY timestamp DESC
"""
)
suspend fun getErrorDetails(
start: Instant,
end: Instant,
version: String,
message: String,
domain: String,
): List<ErrorEntity>
suspend fun getNoNoise(): List<ErrorEntity>
}
40 changes: 38 additions & 2 deletions api/server/src/main/kotlin/database/ErrorEntity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package de.binarynoise.captiveportalautologin.server.database

import kotlin.time.Instant
import kotlinx.datetime.TimeZone.Companion.UTC
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.jetbrains.kotlinx.dataframe.annotations.DataSchema

@Entity(tableName = "errors")
data class ErrorEntity(
@DataSchema
open class ErrorEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val version: String,
val timestamp: Instant,
Expand All @@ -14,4 +19,35 @@ data class ErrorEntity(
val message: String,
val solver: String,
val stackTrace: String,
)
) {
fun toExtendedErrorEntity() = ExtendedErrorEntity(
id = id,
version = version,
timestamp = timestamp,
ssid = ssid,
url = url,
message = message,
solver = solver,
stackTrace = stackTrace,
domain = url.getUrlDomain(),
majorVersion = version.getMajorVersion(),
year = timestamp.toLocalDateTime(UTC).year,
month = timestamp.toLocalDateTime(UTC).month.number,
)
}

@DataSchema
class ExtendedErrorEntity(
id: Long = 0,
version: String,
timestamp: Instant,
ssid: String,
url: String,
message: String,
solver: String,
stackTrace: String,
val domain: String,
val majorVersion: Int,
val year: Int,
val month: Int,
) : ErrorEntity(id, version, timestamp, ssid, url, message, solver, stackTrace)
14 changes: 1 addition & 13 deletions api/server/src/main/kotlin/database/SuccessDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,5 @@ interface SuccessDao {
suspend fun insertOrIncrement(version: String, year: Int, month: Int, ssid: String, url: String, solver: String)

@Query("SELECT * FROM successes")
suspend fun getAllSuccesses(): List<SuccessEntity>

@Query(
"""
SELECT * FROM successes
WHERE year = :year
AND month = :month
AND version LIKE '%' || :version || '%'
AND url LIKE '%' || :domain || '%'
ORDER BY count DESC, ssid ASC, url ASC
"""
)
suspend fun getSuccessDetails(year: Int, month: Int, version: String, domain: String): List<SuccessEntity>
suspend fun getAll(): List<SuccessEntity>
}
31 changes: 29 additions & 2 deletions api/server/src/main/kotlin/database/SuccessEntity.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
package de.binarynoise.captiveportalautologin.server.database

import androidx.room.Entity
import org.jetbrains.kotlinx.dataframe.annotations.DataSchema

@Entity(
tableName = "successes",
primaryKeys = ["version", "year", "month", "ssid", "url", "solver"],
)
data class SuccessEntity(
@DataSchema
open class SuccessEntity(
val version: String,
val year: Int,
val month: Int,
val ssid: String,
val url: String,
val solver: String,
val count: Int,
)
) {
fun toExtendedSuccessEntity() = ExtendedSuccessEntity(
version,
year,
month,
ssid,
url,
solver,
count,
url.getUrlDomain(),
version.getMajorVersion(),
)
}

@DataSchema
class ExtendedSuccessEntity(
version: String,
year: Int,
month: Int,
ssid: String,
url: String,
solver: String,
count: Int,
val domain: String,
val majorVersion: Int,
) : SuccessEntity(version, year, month, ssid, url, solver, count)
8 changes: 8 additions & 0 deletions api/server/src/main/kotlin/database/utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.binarynoise.captiveportalautologin.server.database

import io.ktor.http.URLBuilder


fun String.getUrlDomain(): String = if (isNotEmpty()) URLBuilder(urlString = this).host else ""

fun String.getMajorVersion(): Int = this.split('-', '+').first().toInt()
Loading
Loading