Add XTEA key export endpoint

Signed-off-by: Graham <gpe@openrs2.org>
pull/132/head
Graham 3 years ago
parent 936968363b
commit 412d6f4c1f
  1. 2
      archive/build.gradle.kts
  2. 2
      archive/src/main/kotlin/org/openrs2/archive/ArchiveModule.kt
  3. 59
      archive/src/main/kotlin/org/openrs2/archive/cache/CacheExporter.kt
  4. 10
      archive/src/main/kotlin/org/openrs2/archive/web/CachesController.kt
  5. 13
      archive/src/main/kotlin/org/openrs2/archive/web/WebServer.kt
  6. 3
      archive/src/main/resources/org/openrs2/archive/templates/caches/index.html

@ -16,10 +16,12 @@ dependencies {
implementation(project(":cli"))
implementation(project(":db"))
implementation(project(":json"))
implementation(project(":json"))
implementation(project(":net"))
implementation(project(":protocol"))
implementation(project(":util"))
implementation("com.google.guava:guava:${Versions.guava}")
implementation("io.ktor:ktor-jackson:${Versions.ktor}")
implementation("io.ktor:ktor-server-netty:${Versions.ktor}")
implementation("io.ktor:ktor-thymeleaf:${Versions.ktor}")
implementation("org.flywaydb:flyway-core:${Versions.flyway}")

@ -3,6 +3,7 @@ package org.openrs2.archive
import com.google.inject.AbstractModule
import com.google.inject.Scopes
import org.openrs2.buffer.BufferModule
import org.openrs2.cache.CacheModule
import org.openrs2.db.Database
import org.openrs2.json.JsonModule
import org.openrs2.net.NetworkModule
@ -10,6 +11,7 @@ import org.openrs2.net.NetworkModule
public object ArchiveModule : AbstractModule() {
override fun configure() {
install(BufferModule)
install(CacheModule)
install(JsonModule)
install(NetworkModule)

@ -5,6 +5,7 @@ import io.netty.buffer.Unpooled
import org.openrs2.buffer.use
import org.openrs2.cache.Js5Archive
import org.openrs2.cache.Store
import org.openrs2.crypto.XteaKey
import org.openrs2.db.Database
import java.time.Instant
import javax.inject.Inject
@ -24,6 +25,14 @@ public class CacheExporter @Inject constructor(
val name: String?
)
public data class GroupKey(
val archive: Int,
val group: Int,
val nameHash: Int?,
val name: String?,
val key: XteaKey
)
public suspend fun list(): List<Cache> {
return database.execute { connection ->
connection.prepareStatement(
@ -116,6 +125,56 @@ public class CacheExporter @Inject constructor(
}
}
public suspend fun exportKeys(id: Long): List<GroupKey> {
return database.execute { connection ->
connection.prepareStatement(
"""
WITH t AS (
SELECT a.archive_id, c.data, g.container_id
FROM master_indexes m
JOIN master_index_archives a ON a.container_id = m.container_id
JOIN groups g ON g.archive_id = 255 AND g.group_id = a.archive_id::INTEGER AND g.truncated_version = a.version & 65535
JOIN containers c ON c.id = g.container_id AND c.crc32 = a.crc32
JOIN indexes i ON i.container_id = g.container_id AND i.version = a.version
WHERE m.container_id = ?
)
SELECT t.archive_id, ig.group_id, ig.name_hash, n.name, (k.key).k0, (k.key).k1, (k.key).k2, (k.key).k3
FROM t
JOIN index_groups ig ON ig.container_id = t.container_id
JOIN groups g ON g.archive_id = t.archive_id::INTEGER AND g.group_id = ig.group_id AND g.truncated_version = ig.version & 65535
JOIN containers c ON c.id = g.container_id AND c.crc32 = ig.crc32
JOIN keys k ON k.id = c.key_id
LEFT JOIN names n ON n.hash = ig.name_hash AND n.name ~ '^l(?:[0-9]|[1-9][0-9])_(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$'
""".trimIndent()
).use { stmt ->
stmt.setLong(1, id)
stmt.executeQuery().use { rows ->
val keys = mutableListOf<GroupKey>()
while (rows.next()) {
val archive = rows.getInt(1)
val group = rows.getInt(2)
var nameHash: Int? = rows.getInt(3)
if (rows.wasNull()) {
nameHash = null
}
val name = rows.getString(4)
val k0 = rows.getInt(5)
val k1 = rows.getInt(6)
val k2 = rows.getInt(7)
val k3 = rows.getInt(8)
keys += GroupKey(archive, group, nameHash, name, XteaKey(k0, k1, k2, k3))
}
keys
}
}
}
}
private companion object {
private const val BATCH_SIZE = 1024
}

@ -38,4 +38,14 @@ public class CachesController @Inject constructor(
}
}
}
public suspend fun exportKeys(call: ApplicationCall) {
val id = call.parameters["id"]?.toLongOrNull()
if (id == null) {
call.respond(HttpStatusCode.NotFound)
return
}
call.respond(exporter.exportKeys(id))
}
}

@ -1,12 +1,17 @@
package org.openrs2.archive.web
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.jackson.JacksonConverter
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.openrs2.json.Json
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect
import org.thymeleaf.templatemode.TemplateMode
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
@ -16,10 +21,15 @@ import javax.inject.Singleton
@Singleton
public class WebServer @Inject constructor(
private val cachesController: CachesController,
private val keysController: KeysController
private val keysController: KeysController,
@Json private val mapper: ObjectMapper
) {
public fun start() {
embeddedServer(Netty, port = 8000) {
install(ContentNegotiation) {
register(ContentType.Application.Json, JacksonConverter(mapper))
}
install(Thymeleaf) {
addDialect(Java8TimeDialect())
@ -32,6 +42,7 @@ public class WebServer @Inject constructor(
routing {
get("/caches") { cachesController.index(call) }
get("/caches/{id}.zip") { cachesController.export(call) }
get("/caches/{id}.json") { cachesController.exportKeys(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) }

@ -21,7 +21,8 @@
<td th:text="${#temporals.formatISO(cache.timestamp)}"></td>
<td th:text="${cache.name}"></td>
<td>
<a th:href="${'/caches/' + cache.id + '.zip'}">Download</a>
<a th:href="${'/caches/' + cache.id + '.zip'}">Cache</a>
<a th:href="${'/caches/' + cache.id + '.json'}">Keys</a>
</td>
</tr>
</tbody>

Loading…
Cancel
Save