From 47127113f4d7d97752941b28b9f6612929aa3c79 Mon Sep 17 00:00:00 2001 From: Graham Date: Mon, 1 Feb 2021 19:17:39 +0000 Subject: [PATCH] Add initial archiving service web interface Signed-off-by: Graham --- archive/build.gradle.kts | 4 ++ .../org/openrs2/archive/ArchiveCommand.kt | 4 +- .../openrs2/archive/cache/CacheExporter.kt | 44 +++++++++++++++++++ .../openrs2/archive/web/CachesController.kt | 41 +++++++++++++++++ .../org/openrs2/archive/web/KeysController.kt | 29 ++++++++++++ .../org/openrs2/archive/web/WebCommand.kt | 13 ++++++ .../org/openrs2/archive/web/WebServer.kt | 41 +++++++++++++++++ .../archive/templates/caches/index.html | 29 ++++++++++++ buildSrc/src/main/kotlin/Versions.kt | 3 ++ 9 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt create mode 100644 archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt create mode 100644 archive/src/main/kotlin/org/openrs2/archive/web/WebCommand.kt create mode 100644 archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt create mode 100644 archive/src/main/resources/org/openrs2/archive/templates/caches/index.html diff --git a/archive/build.gradle.kts b/archive/build.gradle.kts index 66dbecf1..3461ca86 100644 --- a/archive/build.gradle.kts +++ b/archive/build.gradle.kts @@ -20,9 +20,13 @@ dependencies { implementation(project(":protocol")) implementation(project(":util")) implementation("com.google.guava:guava:${Versions.guava}") + implementation("io.ktor:ktor-server-netty:${Versions.ktor}") + implementation("io.ktor:ktor-thymeleaf:${Versions.ktor}") implementation("org.flywaydb:flyway-core:${Versions.flyway}") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinCoroutines}") implementation("org.postgresql:postgresql:${Versions.postgres}") + implementation("org.thymeleaf:thymeleaf:${Versions.thymeleaf}") + implementation("org.thymeleaf.extras:thymeleaf-extras-java8time:${Versions.thymeleafJava8Time}") } publishing { diff --git a/archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt b/archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt index 95ce488d..48f9d3c9 100644 --- a/archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt +++ b/archive/src/main/kotlin/org/openrs2/archive/ArchiveCommand.kt @@ -5,6 +5,7 @@ import com.github.ajalt.clikt.core.subcommands import org.openrs2.archive.cache.CacheCommand import org.openrs2.archive.key.KeyCommand import org.openrs2.archive.name.NameCommand +import org.openrs2.archive.web.WebCommand public fun main(args: Array): Unit = ArchiveCommand().main(args) @@ -13,7 +14,8 @@ public class ArchiveCommand : NoOpCliktCommand(name = "archive") { subcommands( CacheCommand(), KeyCommand(), - NameCommand() + NameCommand(), + WebCommand() ) } } diff --git a/archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt b/archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt index 669b6063..b0f34ab3 100644 --- a/archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt +++ b/archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt @@ -6,6 +6,7 @@ import org.openrs2.buffer.use import org.openrs2.cache.Js5Archive import org.openrs2.cache.Store import org.openrs2.db.Database +import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -14,6 +15,49 @@ public class CacheExporter @Inject constructor( private val database: Database, private val alloc: ByteBufAllocator ) { + public data class Cache( + val id: Long, + val whirlpool: ByteArray, + val game: String, + val build: Int?, + val timestamp: Instant? + ) + + public suspend fun list(): List { + return database.execute { connection -> + connection.prepareStatement( + """ + SELECT c.id, c.whirlpool, g.name, m.build, m.timestamp + FROM master_indexes m + JOIN games g ON g.id = m.game_id + JOIN containers c ON c.id = m.container_id + ORDER BY g.name ASC, m.build ASC, m.timestamp ASC + """.trimIndent() + ).use { stmt -> + stmt.executeQuery().use { rows -> + val caches = mutableListOf() + + while (rows.next()) { + val id = rows.getLong(1) + val whirlpool = rows.getBytes(2) + val game = rows.getString(3) + + var build: Int? = rows.getInt(4) + if (rows.wasNull()) { + build = null + } + + val timestamp = rows.getTimestamp(5)?.toInstant() + + caches += Cache(id, whirlpool, game, build, timestamp) + } + + caches + } + } + } + } + public suspend fun export(id: Long, store: Store) { // TODO(gpe): think about what to do if there is a collision database.execute { connection -> diff --git a/archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt b/archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt new file mode 100644 index 00000000..fe7a2fde --- /dev/null +++ b/archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt @@ -0,0 +1,41 @@ +package org.openrs2.archive.web + +import io.ktor.application.ApplicationCall +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.response.header +import io.ktor.response.respond +import io.ktor.response.respondOutputStream +import io.ktor.thymeleaf.ThymeleafContent +import io.netty.buffer.ByteBufAllocator +import org.openrs2.archive.cache.CacheExporter +import org.openrs2.cache.DiskStoreZipWriter +import java.util.zip.ZipOutputStream +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +public class CachesController @Inject constructor( + private val exporter: CacheExporter, + private val alloc: ByteBufAllocator +) { + public suspend fun index(call: ApplicationCall) { + val caches = exporter.list() + call.respond(ThymeleafContent("caches/index.html", mapOf("caches" to caches))) + } + + public suspend fun export(call: ApplicationCall) { + val id = call.parameters["id"]?.toLongOrNull() + if (id == null) { + call.respond(HttpStatusCode.NotFound) + return + } + + call.response.header("Content-Disposition", "attachment; filename=\"cache.zip\"") + call.respondOutputStream(contentType = ContentType.Application.Zip) { + DiskStoreZipWriter(ZipOutputStream(this), alloc = alloc).use { store -> + exporter.export(id, store) + } + } + } +} diff --git a/archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt b/archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt new file mode 100644 index 00000000..e7f690fe --- /dev/null +++ b/archive/src/main/kotlin/org/openrs2/archive/web/KeysController.kt @@ -0,0 +1,29 @@ +package org.openrs2.archive.web + +import io.ktor.application.ApplicationCall +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import org.openrs2.archive.key.KeyImporter +import org.openrs2.crypto.XteaKey +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +public class KeysController @Inject constructor( + private val importer: KeyImporter +) { + public suspend fun create(call: ApplicationCall) { + val k0 = call.request.queryParameters["key1"]?.toIntOrNull() + val k1 = call.request.queryParameters["key2"]?.toIntOrNull() + val k2 = call.request.queryParameters["key3"]?.toIntOrNull() + val k3 = call.request.queryParameters["key4"]?.toIntOrNull() + + if (k0 == null || k1 == null || k2 == null || k3 == null) { + call.respond(HttpStatusCode.BadRequest) + return + } + + importer.import(listOf(XteaKey(k0, k1, k2, k3))) + call.respond(HttpStatusCode.NoContent) + } +} diff --git a/archive/src/main/kotlin/org/openrs2/archive/web/WebCommand.kt b/archive/src/main/kotlin/org/openrs2/archive/web/WebCommand.kt new file mode 100644 index 00000000..84f2b593 --- /dev/null +++ b/archive/src/main/kotlin/org/openrs2/archive/web/WebCommand.kt @@ -0,0 +1,13 @@ +package org.openrs2.archive.web + +import com.github.ajalt.clikt.core.CliktCommand +import com.google.inject.Guice +import org.openrs2.archive.ArchiveModule + +public class WebCommand : CliktCommand(name = "web") { + override fun run() { + val injector = Guice.createInjector(ArchiveModule) + val server = injector.getInstance(WebServer::class.java) + server.start() + } +} diff --git a/archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt b/archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt new file mode 100644 index 00000000..90a021e9 --- /dev/null +++ b/archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt @@ -0,0 +1,41 @@ +package org.openrs2.archive.web + +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.routing.get +import io.ktor.routing.routing +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.thymeleaf.Thymeleaf +import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect +import org.thymeleaf.templatemode.TemplateMode +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +public class WebServer @Inject constructor( + private val cachesController: CachesController, + private val keysController: KeysController +) { + public fun start() { + embeddedServer(Netty, port = 8000) { + install(Thymeleaf) { + addDialect(Java8TimeDialect()) + + setTemplateResolver(ClassLoaderTemplateResolver().apply { + prefix = "/org/openrs2/archive/templates/" + templateMode = TemplateMode.HTML + }) + } + + routing { + get("/caches") { cachesController.index(call) } + get("/caches/{id}.zip") { cachesController.export(call) } + + // ideally we'd use POST /keys here, but I want to be compatible with the RuneLite/OpenOSRS API + get("/keys/submit") { keysController.create(call) } + } + }.start(wait = true) + } +} diff --git a/archive/src/main/resources/org/openrs2/archive/templates/caches/index.html b/archive/src/main/resources/org/openrs2/archive/templates/caches/index.html new file mode 100644 index 00000000..2ab01876 --- /dev/null +++ b/archive/src/main/resources/org/openrs2/archive/templates/caches/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + +
Master Index DigestGameBuildTimestamp
00000000runescape550 + Download +
+ + + diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 372f4a52..59efd8df 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -22,12 +22,15 @@ object Versions { const val kotlin = "1.4.21-2" const val kotlinCoroutines = "1.4.2" const val kotlinter = "3.3.0" + const val ktor = "1.5.1" const val logback = "1.2.3" const val netty = "4.1.58.Final" const val nettyIoUring = "0.0.3.Final" const val openrs2Natives = "3.2.0" const val postgres = "42.2.18" const val shadowPlugin = "6.1.0" + const val thymeleaf = "3.0.12.RELEASE" + const val thymeleafJava8Time = "3.0.4.RELEASE" const val versionsPlugin = "0.36.0" const val xz = "1.8" }