From 0e6a70d75321362e629db8fd733617a670f887e8 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Sun, 22 Dec 2024 14:30:06 -0500 Subject: [PATCH] Various NS auth and WebDAV token improvements --- libs/nsapi4j.jar | Bin 1533117 -> 1533625 bytes .../kotlin/info/mechyrdia/Configuration.kt | 11 +- src/main/kotlin/info/mechyrdia/Factbooks.kt | 7 +- .../info/mechyrdia/auth/NationStates.kt | 10 +- .../kotlin/info/mechyrdia/auth/ViewsLogin.kt | 46 +++---- src/main/kotlin/info/mechyrdia/auth/WebDav.kt | 78 +++++++++-- src/main/kotlin/info/mechyrdia/data/Data.kt | 6 +- .../kotlin/info/mechyrdia/data/DataFiles.kt | 4 +- .../kotlin/info/mechyrdia/data/Nations.kt | 130 +++++++++++++++--- .../info/mechyrdia/data/ViewComments.kt | 8 +- .../info/mechyrdia/data/ViewsComment.kt | 4 +- .../kotlin/info/mechyrdia/data/ViewsFiles.kt | 16 +-- .../kotlin/info/mechyrdia/data/ViewsUser.kt | 38 ++--- .../info/mechyrdia/lore/ParserPreprocess.kt | 2 +- .../kotlin/info/mechyrdia/lore/ViewsRss.kt | 3 +- .../info/mechyrdia/route/ResourceBodies.kt | 5 + .../info/mechyrdia/route/ResourceCsrf.kt | 3 +- .../info/mechyrdia/route/ResourceTypes.kt | 35 +++-- .../info/mechyrdia/route/ResourceWebDav.kt | 35 +++-- src/main/resources/static/admin.js | 37 +++++ 20 files changed, 338 insertions(+), 140 deletions(-) diff --git a/libs/nsapi4j.jar b/libs/nsapi4j.jar index a80a0c13c6987c49d7c142a8b7131434a467892c..8f0cd7d4649e733af223cc50012030907e13714f 100644 GIT binary patch delta 36723 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 delta 36262 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 z zMj%uxHv?r~gH73(sSAzR1qzaqeMKw!IdF89>9#20cVuTRlWfHnZ*;J`1KYasTHKBO zH=SrdVHY=?Cm+UgZcJ>x3GBaH?4~bR9VU{MRnGC+ayXM4nf_;`O#c_Q6KV96AIHAo zuNoRhzhTON7Du;%+A!hJ3`q1`xp24*t7mv|Hb_bAj9Of)faӪ$&A{YDx2D1?E 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 c5A$@fX0&vGIxc{#^s-V}X~hcLzV!t4ezga7~l diff --git a/src/main/kotlin/info/mechyrdia/Configuration.kt b/src/main/kotlin/info/mechyrdia/Configuration.kt index 673d442..413fd54 100644 --- a/src/main/kotlin/info/mechyrdia/Configuration.kt +++ b/src/main/kotlin/info/mechyrdia/Configuration.kt @@ -3,6 +3,7 @@ package info.mechyrdia import info.mechyrdia.auth.Argon2Hasher import info.mechyrdia.data.Id import info.mechyrdia.data.NationData +import info.mechyrdia.data.NationUrlSlug import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer @@ -14,6 +15,7 @@ import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonPrimitive import java.io.File import java.nio.charset.Charset @@ -77,7 +79,9 @@ data class Configuration( val dbName: String = "nslore", val dbConn: String = "mongodb://localhost:27017", - val ownerNation: String = "mechyrdia", + val ownerNation: String = "1593419", + + val emergencyUsername: NationUrlSlug = NationUrlSlug("mechyrdia"), @Serializable(with = StoredPasswordConfigJsonSerializer::class) val emergencyPassword: StoredPassword? = null, @@ -122,7 +126,10 @@ object StoredPasswordConfigJsonSerializer : KSerializer { get() = buildSerialDescriptor("StoredPasswordConfigJsonSerializer", PolymorphicKind.SEALED) override fun serialize(encoder: Encoder, value: StoredPassword) { - defaultSerializer.serialize(encoder, value) + if (encoder is JsonEncoder) + encoder.encodeJsonElement(encoder.json.encodeToJsonElement(defaultSerializer, value)) + else + defaultSerializer.serialize(encoder, value) } override fun deserialize(decoder: Decoder): StoredPassword { diff --git a/src/main/kotlin/info/mechyrdia/Factbooks.kt b/src/main/kotlin/info/mechyrdia/Factbooks.kt index 5df15c3..20e0bbc 100644 --- a/src/main/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/main/kotlin/info/mechyrdia/Factbooks.kt @@ -122,9 +122,7 @@ fun Application.factbooks() { generate { "call_${counter.incrementAndGet().toULong()}_${System.currentTimeMillis()}" } - reply { call, callId -> - call.response.header("X-Call-Id", callId) - } + replyToHeader("X-Call-Id") } install(CallLogging) { @@ -272,7 +270,7 @@ fun Application.factbooks() { get() post() get() - get() + get() post() post() get() @@ -283,6 +281,7 @@ fun Application.factbooks() { get() get() post() + post() get() post() postMultipart() diff --git a/src/main/kotlin/info/mechyrdia/auth/NationStates.kt b/src/main/kotlin/info/mechyrdia/auth/NationStates.kt index b7fab6d..9b6b29a 100644 --- a/src/main/kotlin/info/mechyrdia/auth/NationStates.kt +++ b/src/main/kotlin/info/mechyrdia/auth/NationStates.kt @@ -1,10 +1,11 @@ package info.mechyrdia.auth -import com.aventrix.jnanoid.jnanoid.NanoIdUtils import com.github.agadar.nationstates.DefaultNationStatesImpl import com.github.agadar.nationstates.NationStates import com.github.agadar.nationstates.exception.NationStatesResourceNotFoundException import com.github.agadar.nationstates.query.APIQuery +import info.mechyrdia.data.NationUrlSlug +import info.mechyrdia.data.nanoId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible @@ -18,9 +19,10 @@ suspend fun , R> Q.executeSuspend(): R? = runInterruptible(Di } } -fun String.toNationId() = replace(' ', '_').lowercase() +fun String.toNationSlug() = NationUrlSlug(replace(' ', '_').lowercase()) -private val tokenAlphabet = "ABCDEFGHILMNOPQRSTVXYZ0123456789".toCharArray() -fun token(): String = NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, tokenAlphabet, 16) +fun token(): String = nanoId(16) + +fun gigaToken(): String = nanoId(32) class ForbiddenException(override val message: String) : RuntimeException(message) diff --git a/src/main/kotlin/info/mechyrdia/auth/ViewsLogin.kt b/src/main/kotlin/info/mechyrdia/auth/ViewsLogin.kt index 4408087..847d8ac 100644 --- a/src/main/kotlin/info/mechyrdia/auth/ViewsLogin.kt +++ b/src/main/kotlin/info/mechyrdia/auth/ViewsLogin.kt @@ -1,13 +1,12 @@ package info.mechyrdia.auth -import com.github.agadar.nationstates.shard.NationShard -import info.mechyrdia.Configuration import info.mechyrdia.data.DataDocument import info.mechyrdia.data.DocumentTable import info.mechyrdia.data.Id import info.mechyrdia.data.InstantSerializer import info.mechyrdia.data.MONGODB_ID_KEY import info.mechyrdia.data.NationData +import info.mechyrdia.data.NationVerifyResult import info.mechyrdia.data.TableHolder import info.mechyrdia.lore.page import info.mechyrdia.lore.redirectHref @@ -23,7 +22,19 @@ import io.ktor.server.sessions.clear import io.ktor.server.sessions.sessions import io.ktor.server.sessions.set import io.ktor.util.AttributeKey -import kotlinx.html.* +import kotlinx.html.FormMethod +import kotlinx.html.HTML +import kotlinx.html.br +import kotlinx.html.button +import kotlinx.html.form +import kotlinx.html.h1 +import kotlinx.html.hiddenInput +import kotlinx.html.label +import kotlinx.html.p +import kotlinx.html.section +import kotlinx.html.style +import kotlinx.html.submitInput +import kotlinx.html.textInput import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.time.Instant @@ -73,16 +84,14 @@ suspend fun ApplicationCall.loginPage(): HTML.() -> Unit { form(method = FormMethod.post, action = href(Root.Auth.LoginPost())) { installCsrfToken(call = this@loginPage) - hiddenInput { - name = "tokenId" + hiddenInput(name = "tokenId") { value = tokenId } label { +"Nation Name" br - textInput { - name = "nation" + textInput(name = "nation") { placeholder = "Name of your nation without pretitle, e.g. Mechyrdia, Valentine Z, Reinkalistan, etc." } } @@ -96,8 +105,7 @@ suspend fun ApplicationCall.loginPage(): HTML.() -> Unit { label { +"Verification Checksum" br - textInput { - name = "checksum" + textInput(name = "checksum") { placeholder = "The random text checksum generated by NationStates for verification" } } @@ -108,24 +116,14 @@ suspend fun ApplicationCall.loginPage(): HTML.() -> Unit { } suspend fun ApplicationCall.loginRoute(nation: String, checksum: String, tokenId: String): Nothing { - val nationId = nation.toNationId() + val nationSlug = nation.toNationSlug() val nsToken = NsStoredToken.verifyToken(tokenId) ?: throw MissingRequestParameterException("tokenId") - val nationData = if (nationId == Configuration.Current.ownerNation && checksum == Configuration.Current.emergencyPassword) - NationData.get(Id(nationId)) - else { - val result = NSAPI - .verifyAndGetNation(nationId, checksum) - .token("mechyrdia_$nsToken") - .shards(NationShard.NAME, NationShard.FLAG_URL) - .executeSuspend() - ?: redirectHrefWithError(Root.Auth.LoginPage(), error = "That nation does not exist.") - - if (!result.isVerified) - redirectHrefWithError(Root.Auth.LoginPage(), error = "Checksum failed verification.") - - NationData(Id(result.id), result.name, result.flagUrl).also { NationData.Table.put(it) } + val nationData = when (val result = NationData.verify(nationSlug, nsToken, checksum)) { + NationVerifyResult.NationDoesNotExist -> redirectHrefWithError(Root.Auth.LoginPage(), error = "That nation does not exist.") + NationVerifyResult.ChecksumFailedVerification -> redirectHrefWithError(Root.Auth.LoginPage(), error = "Checksum failed verification.") + is NationVerifyResult.LoginSuccess -> result.nation } sessions.set(UserSession(nationData.id)) diff --git a/src/main/kotlin/info/mechyrdia/auth/WebDav.kt b/src/main/kotlin/info/mechyrdia/auth/WebDav.kt index 92c8d97..e9a51b0 100644 --- a/src/main/kotlin/info/mechyrdia/auth/WebDav.kt +++ b/src/main/kotlin/info/mechyrdia/auth/WebDav.kt @@ -13,10 +13,12 @@ import info.mechyrdia.data.currentNation import info.mechyrdia.data.serialName import info.mechyrdia.lore.adminPage import info.mechyrdia.lore.dateTime +import info.mechyrdia.lore.redirectHref import info.mechyrdia.lore.redirectHrefWithError import info.mechyrdia.route.Root import info.mechyrdia.route.href import info.mechyrdia.route.installCsrfToken +import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall import kotlinx.coroutines.flow.toList import kotlinx.html.* @@ -28,6 +30,7 @@ import java.time.Instant data class WebDavToken( @SerialName(MONGODB_ID_KEY) override val id: Id = Id(), + val pwHash: String, val holder: Id, val validUntil: @Serializable(with = InstantSerializer::class) Instant @@ -66,21 +69,28 @@ suspend fun ApplicationCall.adminRequestWebDavToken(): HTML.() -> Unit { table { tr { - th { +"Token" } - th { +"Expires at" } + th(ThScope.col) { +"Token Name" } + th(ThScope.col) { +"Expires at" } + th(ThScope.col) { +Entities.nbsp } } for (existingToken in existingTokens) { tr { td { - textInput { - readonly = true - value = existingToken.id.id + code { + +existingToken.id.id } } td { dateTime(existingToken.validUntil) } + td { + form(method = FormMethod.post, action = href(Root.Admin.Vfs.WebDavTokenDelete())) { + installCsrfToken(call = this@adminRequestWebDavToken) + hiddenInput(name = "tokenId") { value = existingToken.id.id } + submitInput(classes = "evil") { value = "Delete Token" } + } + } } } } @@ -94,9 +104,12 @@ suspend fun ApplicationCall.adminObtainWebDavToken(): HTML.() -> Unit { val nation = currentNation() ?: redirectHrefWithError(Root.Auth.LoginPage(), error = "You must be logged in to generate WebDAV tokens") + val tokenPw = gigaToken() + val token = WebDavToken( holder = nation.id, - validUntil = Instant.now().plusSeconds(86_400) + pwHash = Argon2Hasher.createHash(tokenPw), + validUntil = Instant.now().plusSeconds(31_556_925) ) WebDavToken.Table.put(token) @@ -106,15 +119,54 @@ suspend fun ApplicationCall.adminObtainWebDavToken(): HTML.() -> Unit { h1 { +"Your New WebDAV Token" } div { style = "text-align:center" - textInput { - readonly = true - value = token.id.id - } - p { - +"Your new token will expire at " - dateTime(token.validUntil) + table { + tr { + th(ThScope.row) { + style = "text-align:right" + +"Username" + } + td { + a(href = "#", classes = "text-copy") { + +token.id.id + } + } + } + tr { + th(ThScope.row) { + style = "text-align:right" + +"Password" + } + td { + a(href = "#", classes = "text-copy") { + +tokenPw + } + } + } + tr { + th(ThScope.row) { + style = "text-align:right" + +"Expiration" + } + td { + dateTime(token.validUntil) + } + } } } } } } + +suspend fun ApplicationCall.adminDeleteWebDavToken(tokenId: Id): Nothing { + val nation = currentNation() ?: redirectHrefWithError(Root.Auth.LoginPage(), error = "You must be logged in to delete WebDAV tokens") + + val token = WebDavToken.Table.get(tokenId) ?: redirectHrefWithError(Root.Admin.Vfs.WebDavTokenPage(), error = "That token does not exist") + + if (token.holder != nation.id) { + redirectHrefWithError(Root.Admin.Vfs.WebDavTokenPage(), error = "You do not own that token") + } + + WebDavToken.Table.del(token.id) + + redirectHref(Root.Admin.Vfs.WebDavTokenPage(), HttpStatusCode.SeeOther) +} diff --git a/src/main/kotlin/info/mechyrdia/data/Data.kt b/src/main/kotlin/info/mechyrdia/data/Data.kt index 4c7f79f..8a55276 100644 --- a/src/main/kotlin/info/mechyrdia/data/Data.kt +++ b/src/main/kotlin/info/mechyrdia/data/Data.kt @@ -47,7 +47,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase as JMongoDatabase @Serializable(IdSerializer::class) @JvmInline -value class Id(val id: String) { +value class Id<@Suppress("unused") T>(val id: String) { override fun toString() = id companion object { @@ -57,7 +57,9 @@ value class Id(val id: String) { private val secureRandom = SecureRandom.getInstanceStrong() private val alphabet = "ABCDEFGHILMNOPQRSTVXYZ0123456789".toCharArray() -fun Id() = Id(NanoIdUtils.randomNanoId(secureRandom, alphabet, 24)) +fun nanoId(length: Int): String = NanoIdUtils.randomNanoId(secureRandom, alphabet, length) + +fun Id() = Id(nanoId(24)) object IdSerializer : KSerializer> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Id", PrimitiveKind.STRING) diff --git a/src/main/kotlin/info/mechyrdia/data/DataFiles.kt b/src/main/kotlin/info/mechyrdia/data/DataFiles.kt index 7e081d7..6813dfa 100644 --- a/src/main/kotlin/info/mechyrdia/data/DataFiles.kt +++ b/src/main/kotlin/info/mechyrdia/data/DataFiles.kt @@ -294,8 +294,8 @@ private data class GridFsEntry( ) : DataDocument private class GridFsStorage(val table: DocumentTable, val bucket: GridFSBucket) : FileStorage { - private fun toExactPath(path: StoragePath) = path.elements.concat("/", prefix = "/") - private fun toPrefixPath(path: StoragePath) = path.elements.concat("/", prefix = "/", suffix = "/") + private fun toExactPath(path: StoragePath) = path.elements.map { "/$it" }.concat() + private fun toPrefixPath(path: StoragePath) = path.elements.map { "/$it" }.concat(suffix = "/") private suspend fun testExact(path: StoragePath) = table.number(Filters.eq(GridFsEntry::path.serialName, toExactPath(path))) > 0L private suspend fun getExact(path: StoragePath) = table.locate(Filters.eq(GridFsEntry::path.serialName, toExactPath(path))) diff --git a/src/main/kotlin/info/mechyrdia/data/Nations.kt b/src/main/kotlin/info/mechyrdia/data/Nations.kt index 800bd27..5f177f8 100644 --- a/src/main/kotlin/info/mechyrdia/data/Nations.kt +++ b/src/main/kotlin/info/mechyrdia/data/Nations.kt @@ -1,6 +1,11 @@ package info.mechyrdia.data +import com.github.agadar.nationstates.domain.nation.Nation +import com.github.agadar.nationstates.domain.nation.NationVerification +import com.github.agadar.nationstates.query.ShardQuery import com.github.agadar.nationstates.shard.NationShard +import com.mongodb.client.model.Filters +import info.mechyrdia.Configuration import info.mechyrdia.OwnerNationId import info.mechyrdia.auth.NSAPI import info.mechyrdia.auth.UserSession @@ -17,10 +22,17 @@ import java.util.concurrent.ConcurrentHashMap private val NationsLogger: Logger = LoggerFactory.getLogger("info.mechyrdia.data.NationsKt") +@Serializable +@JvmInline +value class NationUrlSlug(val slug: String) { + override fun toString() = slug +} + @Serializable data class NationData( @SerialName(MONGODB_ID_KEY) override val id: Id, + val slug: NationUrlSlug, val name: String, val flag: String, @@ -30,44 +42,118 @@ data class NationData( override val Table = DocumentTable() override suspend fun initialize() { + Table.index(NationData::slug.ascending) Table.index(NationData::name.ascending) } - fun unknown(id: Id): NationData { - NationsLogger.warn("Unable to find nation with Id $id - did it CTE?") - return NationData(id, "Unknown Nation", "https://www.nationstates.net/images/flags/exnation.png") + private fun unknownOfDbId(dbId: Id): NationData { + NationsLogger.warn("Unable to find nation with DB ID $dbId") + return NationData( + id = dbId, + slug = NationUrlSlug("unknown_dbid_$dbId"), + name = "Unknown Nation", + flag = "https://www.nationstates.net/images/flags/exnation.png" + ) + } + + private fun unknownOfSlug(slug: NationUrlSlug): NationData { + NationsLogger.warn("Unable to find nation with URL slug $slug") + return NationData( + id = Id("unknown_slug_$slug"), + slug = slug, + name = "Unknown Nation", + flag = "https://www.nationstates.net/images/flags/exnation.png" + ) + } + + suspend fun getByDbId(nationDbId: Id): NationData { + return Table.get(nationDbId) ?: unknownOfDbId(nationDbId) } - suspend fun get(id: Id): NationData = Table.getOrPut(id) { - NSAPI - .getNation(id.id) - .shards(NationShard.NAME, NationShard.FLAG_URL) - .executeSuspend() - ?.let { nation -> - NationData(id = Id(nation.id), name = nation.name, flag = nation.flagUrl) - } ?: unknown(id) + suspend fun getBySlug(nationSlug: NationUrlSlug, queryNsApi: Boolean = true): NationData { + return Table.locate(Filters.eq(NationData::slug.serialName, nationSlug)) + ?: (if (queryNsApi) + NSAPI.getNation(nationSlug.slug) + .defaultShards() + .executeSuspend() + ?.toNationData() + else null) ?: unknownOfSlug(nationSlug) + } + + suspend fun verify(nationSlug: NationUrlSlug, nsToken: String, checksum: String): NationVerifyResult { + val result = if (nationSlug == Configuration.Current.emergencyUsername && Configuration.Current.emergencyPassword?.verify(checksum) == true) { + NSAPI.getNation(nationSlug.slug) + .defaultShards() + .executeSuspend() + } else { + NSAPI.verifyAndGetNation(nationSlug.slug, checksum) + .token("mechyrdia_$nsToken") + .defaultShards() + .executeSuspend() + } + + result ?: return NationVerifyResult.NationDoesNotExist + + if (result is NationVerification && !result.isVerified) + return NationVerifyResult.ChecksumFailedVerification + + val nationData = result.toNationData(Table.get(Id(result.dbId))).also { Table.put(it) } + return NationVerifyResult.LoginSuccess(nationData) } } } -val CallNationCacheAttribute = AttributeKey, NationData>>("Mechyrdia.NationCache") +private fun > Q.defaultShards(): Q = shards(NationShard.DB_ID, NationShard.NAME, NationShard.FLAG_URL) -val ApplicationCall.nationCache: MutableMap, NationData> - get() = attributes.computeIfAbsent(CallNationCacheAttribute) { - ConcurrentHashMap, NationData>() - } +private fun Nation.toNationData(prev: NationData? = null) = NationData( + id = Id(dbId), + slug = NationUrlSlug(id), + name = name, + flag = flagUrl, + isBanned = prev?.isBanned == true +) -suspend fun MutableMap, NationData>.getNation(id: Id): NationData { - return getOrPut(id) { - NationData.get(id) +sealed interface NationVerifyResult { + data object NationDoesNotExist : NationVerifyResult + data object ChecksumFailedVerification : NationVerifyResult + + @JvmInline + value class LoginSuccess(val nation: NationData) : NationVerifyResult +} + +val CallNationCacheAttribute = AttributeKey("Mechyrdia.NationCache") + +class NationCache { + private val bySlug = ConcurrentHashMap() + private val byDbId = ConcurrentHashMap, NationData>() + + suspend fun getBySlug(slug: NationUrlSlug): NationData { + return bySlug.getOrPut(slug) { + NationData.getBySlug(slug, false).also { byDbId.putIfAbsent(it.id, it) } + } + } + + suspend fun getByDbId(dbId: Id): NationData { + return byDbId.getOrPut(dbId) { + NationData.getByDbId(dbId).also { bySlug.putIfAbsent(it.slug, it) } + } } } +val ApplicationCall.nationCache: NationCache + get() = attributes.computeIfAbsent(CallNationCacheAttribute) { + NationCache() + } + private val CallCurrentNationAttribute = AttributeKey("Mechyrdia.CurrentNation") -fun ApplicationCall.ownerNationOnly() { - if (sessions.get()?.nationId != OwnerNationId) +suspend fun ApplicationCall.adminNationOnly(): NationData { + val nationData = currentNation() + + if (nationData?.id != OwnerNationId) throw NoSuchElementException("Hidden page") + + return nationData } suspend fun ApplicationCall.currentNation(): NationData? { @@ -77,7 +163,7 @@ suspend fun ApplicationCall.currentNation(): NationData? { return sessions.get() ?.nationId - ?.let { nationCache.getNation(it) } + ?.let { nationCache.getByDbId(it) } ?.also { attributes.put(CallCurrentNationAttribute, NationSession(it)) } } diff --git a/src/main/kotlin/info/mechyrdia/data/ViewComments.kt b/src/main/kotlin/info/mechyrdia/data/ViewComments.kt index ad24564..e6a9610 100644 --- a/src/main/kotlin/info/mechyrdia/data/ViewComments.kt +++ b/src/main/kotlin/info/mechyrdia/data/ViewComments.kt @@ -36,9 +36,9 @@ data class CommentRenderData( val replyLinks: List>, ) { companion object { - private suspend fun render(comment: Comment, nations: MutableMap, NationData> = mutableMapOf()): CommentRenderData { + private suspend fun render(comment: Comment, nations: NationCache = NationCache()): CommentRenderData { val (nationData, pageTitle, htmlResult) = coroutineScope { - val nationDataAsync = async { nations.getNation(comment.submittedBy) } + val nationDataAsync = async { nations.getByDbId(comment.submittedBy) } val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn).toFriendlyPathTitle() } val htmlResultAsync = async { comment.contents.parseAs(ParserTree::toCommentHtml) } @@ -59,7 +59,7 @@ data class CommentRenderData( ) } - suspend operator fun invoke(comments: List, nations: MutableMap, NationData> = mutableMapOf()): List { + suspend operator fun invoke(comments: List, nations: NationCache = NationCache()): List { return comments.mapSuspend { comment -> render(comment, nations) } @@ -86,7 +86,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id Unit { Comment.Table .sorted(Sorts.descending(Comment::submittedAt.serialName)) .filterNot { comment -> - comment.submittedBy != currNation?.id && NationData.get(comment.submittedBy).isBanned + comment.submittedBy != currNation?.id && NationData.getByDbId(comment.submittedBy).isBanned } .take(limit) .toList(), @@ -103,7 +103,7 @@ suspend fun ApplicationCall.viewCommentRoute(commentId: Id): Nothing { val comment = Comment.Table.get(commentId)!! val currentNation = currentNation() - val submitter = nationCache.getNation(comment.submittedBy) + val submitter = nationCache.getByDbId(comment.submittedBy) if (submitter.isBanned && currentNation?.id != comment.submittedBy && currentNation?.id != OwnerNationId) throw NoSuchElementException("Shadowbanned comment") diff --git a/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt b/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt index 9e2630d..2de8a3b 100644 --- a/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt +++ b/src/main/kotlin/info/mechyrdia/data/ViewsFiles.kt @@ -119,7 +119,7 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit { when (tree) { is TreeNode.FileNode -> table { tr { - th { + th(ThScope.col) { colSpan = "2" +"/$path" } @@ -133,19 +133,19 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit { } } tr { - th { +"Created at" } + th(ThScope.row) { +"Created at" } td { dateTime(tree.stats.created) } } tr { - th { +"Last updated at" } + th(ThScope.row) { +"Last updated at" } td { dateTime(tree.stats.updated) } } tr { - th { +"Size (bytes)" } + th(ThScope.row) { +"Size (bytes)" } td { +"${tree.stats.size}" } } tr { - th { +"Actions" } + th(ThScope.row) { +"Actions" } td { ul { li { @@ -177,7 +177,7 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit { } } tr { - th { +"Navigate" } + th(ThScope.row) { +"Navigate" } td { ul { path.elements.indices.forEach { index -> @@ -311,11 +311,11 @@ suspend fun ApplicationCall.adminConfirmDeleteFile(path: StoragePath) { } table { tr { - th { +"Last Updated" } + th(ThScope.row) { +"Last Updated" } td { dateTime(stats.updated) } } tr { - th { +"Size (bytes)" } + th(ThScope.row) { +"Size (bytes)" } td { +"${stats.size}" } } } diff --git a/src/main/kotlin/info/mechyrdia/data/ViewsUser.kt b/src/main/kotlin/info/mechyrdia/data/ViewsUser.kt index 5f814e1..5d48b69 100644 --- a/src/main/kotlin/info/mechyrdia/data/ViewsUser.kt +++ b/src/main/kotlin/info/mechyrdia/data/ViewsUser.kt @@ -2,7 +2,6 @@ package info.mechyrdia.data import com.mongodb.client.model.Updates import info.mechyrdia.OwnerNationId -import info.mechyrdia.auth.UserSession import info.mechyrdia.lore.NationProfileSidebar import info.mechyrdia.lore.page import info.mechyrdia.lore.redirectHref @@ -12,22 +11,25 @@ import info.mechyrdia.route.href import info.mechyrdia.route.installCsrfToken import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall -import io.ktor.server.sessions.get -import io.ktor.server.sessions.sessions import kotlinx.coroutines.flow.toList -import kotlinx.html.* +import kotlinx.html.HTML +import kotlinx.html.a +import kotlinx.html.h1 +import kotlinx.html.id +import kotlinx.html.p +import kotlinx.html.section -fun ApplicationCall.currentUserPage(): Nothing { - val currNationId = sessions.get()?.nationId - if (currNationId == null) +suspend fun ApplicationCall.currentUserPage(): Nothing { + val currNation = currentNation() + if (currNation == null) redirectHref(Root.Auth.LoginPage(), HttpStatusCode.Found) else - redirectHref(Root.User.ById(currNationId), HttpStatusCode.Found) + redirectHref(Root.User.BySlug(currNation.slug), HttpStatusCode.Found) } -suspend fun ApplicationCall.userPage(userId: Id): HTML.() -> Unit { +suspend fun ApplicationCall.userPage(userSlug: NationUrlSlug): HTML.() -> Unit { val currNation = currentNation() - val viewingNation = nationCache.getNation(userId) + val viewingNation = nationCache.getBySlug(userSlug) val comments = CommentRenderData( Comment.getCommentsBy(viewingNation.id).toList(), @@ -41,13 +43,13 @@ suspend fun ApplicationCall.userPage(userId: Id): HTML.() -> Unit { if (currNation?.id == OwnerNationId) { if (viewingNation.isBanned) { p { +"This user is banned" } - val unbanLink = href(Root.Admin.Unban(viewingNation.id)) + val unbanLink = href(Root.Admin.Unban(viewingNation.slug)) a(href = unbanLink) { installCsrfToken(unbanLink, call = this@userPage) +"Unban" } } else { - val banLink = href(Root.Admin.Ban(viewingNation.id)) + val banLink = href(Root.Admin.Ban(viewingNation.slug)) a(href = banLink) { installCsrfToken(banLink, call = this@userPage) +"Ban" @@ -60,20 +62,20 @@ suspend fun ApplicationCall.userPage(userId: Id): HTML.() -> Unit { } } -suspend fun ApplicationCall.adminBanUserRoute(userId: Id): Nothing { - val bannedNation = nationCache.getNation(userId) +suspend fun ApplicationCall.adminBanUserRoute(userSlug: NationUrlSlug): Nothing { + val bannedNation = nationCache.getBySlug(userSlug) if (!bannedNation.isBanned) NationData.Table.set(bannedNation.id, Updates.set(NationData::isBanned.serialName, true)) - redirectHref(Root.User.ById(userId), HttpStatusCode.SeeOther) + redirectHref(Root.User.BySlug(userSlug), HttpStatusCode.SeeOther) } -suspend fun ApplicationCall.adminUnbanUserRoute(userId: Id): Nothing { - val bannedNation = nationCache.getNation(userId) +suspend fun ApplicationCall.adminUnbanUserRoute(userSlug: NationUrlSlug): Nothing { + val bannedNation = nationCache.getBySlug(userSlug) if (bannedNation.isBanned) NationData.Table.set(bannedNation.id, Updates.set(NationData::isBanned.serialName, false)) - redirectHref(Root.User.ById(userId), HttpStatusCode.SeeOther) + redirectHref(Root.User.BySlug(userSlug), HttpStatusCode.SeeOther) } diff --git a/src/main/kotlin/info/mechyrdia/lore/ParserPreprocess.kt b/src/main/kotlin/info/mechyrdia/lore/ParserPreprocess.kt index 6f1e07d..188e89c 100644 --- a/src/main/kotlin/info/mechyrdia/lore/ParserPreprocess.kt +++ b/src/main/kotlin/info/mechyrdia/lore/ParserPreprocess.kt @@ -48,7 +48,7 @@ class PreProcessorContext private constructor( fun defaults(lorePath: StoragePath) = defaults(lorePath.elements.drop(1)) fun defaults(lorePath: List) = mapOf( - PAGE_PATH_KEY to lorePath.concat("/", prefix = "/").textToTree(), + PAGE_PATH_KEY to lorePath.map { "/$it" }.concat().textToTree(), INSTANT_NOW_KEY to Instant.now().toEpochMilli().numberToTree(), ) } diff --git a/src/main/kotlin/info/mechyrdia/lore/ViewsRss.kt b/src/main/kotlin/info/mechyrdia/lore/ViewsRss.kt index dad60f3..3a7b6f3 100644 --- a/src/main/kotlin/info/mechyrdia/lore/ViewsRss.kt +++ b/src/main/kotlin/info/mechyrdia/lore/ViewsRss.kt @@ -14,7 +14,6 @@ import info.mechyrdia.data.XmlTag import info.mechyrdia.data.XmlTagConsumer import info.mechyrdia.data.currentNation import info.mechyrdia.data.declaration -import info.mechyrdia.data.getNation import info.mechyrdia.data.nationCache import info.mechyrdia.data.respondXml import info.mechyrdia.data.root @@ -150,7 +149,7 @@ suspend fun ApplicationCall.recentCommentsRssFeedGenerator(limit: Int): RssChann if (currNation?.id == OwnerNationId) flow else flow.filterNot { comment -> - comment.submittedBy != currNation?.id && nationCache.getNation(comment.submittedBy).isBanned + comment.submittedBy != currNation?.id && nationCache.getByDbId(comment.submittedBy).isBanned } } .take(limit) diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceBodies.kt b/src/main/kotlin/info/mechyrdia/route/ResourceBodies.kt index bc5507d..1a43760 100644 --- a/src/main/kotlin/info/mechyrdia/route/ResourceBodies.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceBodies.kt @@ -1,5 +1,7 @@ package info.mechyrdia.route +import info.mechyrdia.auth.WebDavToken +import info.mechyrdia.data.Id import info.mechyrdia.lore.TextAlignment import kotlinx.html.* import kotlinx.serialization.Serializable @@ -53,6 +55,9 @@ class AdminVfsCopyFilePayload(val from: String, override val csrfToken: String? @Serializable class AdminVfsRequestWebDavTokenPayload(override val csrfToken: String? = null) : CsrfProtectedResourcePayload +@Serializable +class AdminVfsDeleteWebDavTokenPayload(val tokenId: Id, override val csrfToken: String? = null) : CsrfProtectedResourcePayload + @Serializable class AdminVfsDeleteFilePayload(override val csrfToken: String? = null) : CsrfProtectedResourcePayload diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceCsrf.kt b/src/main/kotlin/info/mechyrdia/route/ResourceCsrf.kt index 27e15be..d9e6330 100644 --- a/src/main/kotlin/info/mechyrdia/route/ResourceCsrf.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceCsrf.kt @@ -36,8 +36,7 @@ fun A.installCsrfToken(route: String = href, call: ApplicationCall) { } fun FORM.installCsrfToken(route: String = action, call: ApplicationCall) { - hiddenInput { - name = "csrfToken" + hiddenInput(name = "csrfToken") { value = call.createCsrfToken(route) } } diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt b/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt index 1db9c1c..24da667 100644 --- a/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceTypes.kt @@ -1,5 +1,6 @@ package info.mechyrdia.route +import info.mechyrdia.auth.adminDeleteWebDavToken import info.mechyrdia.auth.adminObtainWebDavToken import info.mechyrdia.auth.adminRequestWebDavToken import info.mechyrdia.auth.loginPage @@ -8,7 +9,7 @@ import info.mechyrdia.auth.logoutRoute import info.mechyrdia.concat import info.mechyrdia.data.Comment import info.mechyrdia.data.Id -import info.mechyrdia.data.NationData +import info.mechyrdia.data.NationUrlSlug import info.mechyrdia.data.StoragePath import info.mechyrdia.data.adminBanUserRoute import info.mechyrdia.data.adminConfirmDeleteFile @@ -16,6 +17,7 @@ import info.mechyrdia.data.adminConfirmRemoveDirectory import info.mechyrdia.data.adminDeleteFile import info.mechyrdia.data.adminDoCopyFile import info.mechyrdia.data.adminMakeDirectory +import info.mechyrdia.data.adminNationOnly import info.mechyrdia.data.adminOverwriteFile import info.mechyrdia.data.adminPreviewFile import info.mechyrdia.data.adminRemoveDirectory @@ -29,7 +31,6 @@ import info.mechyrdia.data.deleteCommentPage import info.mechyrdia.data.deleteCommentRoute import info.mechyrdia.data.editCommentRoute import info.mechyrdia.data.newCommentRoute -import info.mechyrdia.data.ownerNationOnly import info.mechyrdia.data.recentCommentsPage import info.mechyrdia.data.respondStoredFile import info.mechyrdia.data.respondXml @@ -329,12 +330,12 @@ class Root : ResourceHandler, ResourceFilter { call.currentUserPage() } - @Resource("{id}") - class ById(val id: Id, val user: User = User()) : ResourceHandler { + @Resource("{slug}") + class BySlug(val slug: NationUrlSlug, val user: User = User()) : ResourceHandler { override suspend fun RoutingContext.handleCall() { with(user) { call.filterCall() } - call.respondHtml(HttpStatusCode.OK, call.userPage(id)) + call.respondHtml(HttpStatusCode.OK, call.userPage(slug)) } } } @@ -343,26 +344,26 @@ class Root : ResourceHandler, ResourceFilter { class Admin(val root: Root = Root()) : ResourceFilter { override suspend fun ApplicationCall.filterCall() { with(root) { filterCall() } - ownerNationOnly() + adminNationOnly() } - @Resource("ban/{id}") - class Ban(val id: Id, val admin: Admin = Admin()) : ResourceReceiver { + @Resource("ban/{slug}") + class Ban(val slug: NationUrlSlug, val admin: Admin = Admin()) : ResourceReceiver { override suspend fun RoutingContext.handleCall(payload: AdminBanUserPayload) { with(admin) { call.filterCall() } with(payload) { call.verifyCsrfToken() } - call.adminBanUserRoute(id) + call.adminBanUserRoute(slug) } } - @Resource("unban/{id}") - class Unban(val id: Id, val admin: Admin = Admin()) : ResourceReceiver { + @Resource("unban/{slug}") + class Unban(val slug: NationUrlSlug, val admin: Admin = Admin()) : ResourceReceiver { override suspend fun RoutingContext.handleCall(payload: AdminUnbanUserPayload) { with(admin) { call.filterCall() } with(payload) { call.verifyCsrfToken() } - call.adminUnbanUserRoute(id) + call.adminUnbanUserRoute(slug) } } @@ -456,6 +457,16 @@ class Root : ResourceHandler, ResourceFilter { } } + @Resource("webdav-token/delete") + class WebDavTokenDelete(val vfs: Vfs = Vfs()) : ResourceReceiver { + override suspend fun RoutingContext.handleCall(payload: AdminVfsDeleteWebDavTokenPayload) { + with(vfs) { call.filterCall() } + with(payload) { call.verifyCsrfToken() } + + call.adminDeleteWebDavToken(payload.tokenId) + } + } + @Resource("copy/{path...}") class CopyPage(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { override suspend fun RoutingContext.handleCall() { diff --git a/src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt b/src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt index 95725b2..04eebdc 100644 --- a/src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt +++ b/src/main/kotlin/info/mechyrdia/route/ResourceWebDav.kt @@ -1,8 +1,9 @@ package info.mechyrdia.route +import info.mechyrdia.Configuration import info.mechyrdia.Utf8 +import info.mechyrdia.auth.Argon2Hasher import info.mechyrdia.auth.WebDavToken -import info.mechyrdia.auth.toNationId import info.mechyrdia.concat import info.mechyrdia.data.FileStorage import info.mechyrdia.data.Id @@ -22,6 +23,7 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.log import io.ktor.server.html.respondHtml import io.ktor.server.request.ApplicationRequest import io.ktor.server.request.authorization @@ -42,9 +44,8 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.Base64 -import java.util.UUID -const val WebDavDomainName = "https://dav.mechyrdia.info" +const val WebDavDomainName = "http://localhost:8180" private val dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME @@ -134,7 +135,7 @@ private suspend fun getWebDavPropertiesWithIncludeTags(path: StoragePath, webRoo .filterNotNull() .flatten() - val pathWithSuffix = path.elements.concat("/", suffix = "/") + val pathWithSuffix = path.elements.map { "$it/" }.concat() listOf( WebDavProperties.Collection( creationDate = subProps.mapNotNull { it.first.creationDate }.maxOrNull(), @@ -198,7 +199,7 @@ private val base64Decoder = Base64.getDecoder() fun ApplicationRequest.basicAuth(): Pair? { val auth = authorization() ?: return null - if (!auth.startsWith("Basic ")) return null + if (!auth.startsWith(" ")) return null val basic = auth.substring(6) return String(base64Decoder.decode(basic), Utf8) .split(':', limit = 2) @@ -208,13 +209,16 @@ fun ApplicationRequest.basicAuth(): Pair? { suspend fun ApplicationCall.beforeWebDav() { attributes.put(WebDavAttributeKey, true) - val (user, token) = request.basicAuth() ?: throw WebDavAuthRequired() - val tokenData = WebDavToken.Table.get(Id(token)) ?: throw WebDavAuthRequired() + response.header(HttpHeaders.DAV, "1,2") - if (tokenData.holder.id != user.toNationId() || tokenData.validUntil < Instant.now()) - throw WebDavAuthRequired() + if (Configuration.Current.isDevMode) + return - response.header(HttpHeaders.DAV, "1,2") + val (tokenId, tokenPw) = request.basicAuth() ?: throw WebDavAuthRequired() + val tokenData = WebDavToken.Table.get(Id(tokenId)) ?: throw WebDavAuthRequired() + + if (tokenData.validUntil < Instant.now() || !Argon2Hasher.verifyHash(tokenData.pwHash, tokenPw)) + throw WebDavAuthRequired() } suspend fun ApplicationCall.webDavOptions() { @@ -360,10 +364,8 @@ suspend fun ApplicationCall.webDavDelete(path: StoragePath) { suspend fun ApplicationCall.webDavLock(path: StoragePath) { beforeWebDav() - if (request.header(HttpHeaders.ContentType) != null) - receiveText() - - val depth = request.header(HttpHeaders.Depth) ?: "Infinity" + val lockRequest = receiveText() + application.log.debug(lockRequest) respondXml { declaration() @@ -372,12 +374,9 @@ suspend fun ApplicationCall.webDavLock(path: StoragePath) { "activelock" { "lockscope" { "shared"() } "locktype" { "write"() } - "depth" { +depth } + "depth" { +"0" } "owner"() "timeout" { +"Second-86400" } - "locktoken" { - "href" { +"opaquelocktoken:${UUID.randomUUID()}" } - } } } } diff --git a/src/main/resources/static/admin.js b/src/main/resources/static/admin.js index e6ad241..c935f7c 100644 --- a/src/main/resources/static/admin.js +++ b/src/main/resources/static/admin.js @@ -1,5 +1,18 @@ (function () { + function getCookieMap() { + return document.cookie + .split(";") + .reduce((obj, entry) => { + const trimmed = entry.trim(); + const eqI = trimmed.indexOf('='); + const key = trimmed.substring(0, eqI).trimEnd(); + const value = trimmed.substring(eqI + 1).trimStart(); + return {...obj, [key]: value}; + }, {}); + } + window.addEventListener("load", function () { + // File uploads const fileInputs = document.querySelectorAll("input[type=file]"); for (const fileInput of fileInputs) { fileInput.addEventListener("change", e => { @@ -17,4 +30,28 @@ moment.style.display = "inline"; } }); + + window.addEventListener("load", function () { + // Text copying + const textsToCopy = document.querySelectorAll(".text-copy"); + for (const textToCopy of textsToCopy) { + textToCopy.addEventListener("click", e => { + e.preventDefault(); + navigator.clipboard.writeText(e.currentTarget.innerText) + .catch(reason => { + console.error("Error copying text to clipboard!", reason); + alert("Text copy failed"); + }); + }); + } + }); + + window.addEventListener("load", function () { + // Error popup + const errorMsg = getCookieMap()["ERROR_MSG"]; + if (errorMsg != null) { + document.cookie = "ERROR_MSG=; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax; Secure"; + alert(errorMsg); + } + }); })(); -- 2.25.1