}
post("/me/manage") {
+ val form = call.receiveValidatedParameters()
val currentUser = call.getUser() ?: redirect("/login")
- val form = call.receiveParameters()
val newUser = currentUser.copy(
showDiscordName = form["showdiscord"] == "yes",
}
post("/admiral/new") {
+ val form = call.receiveValidatedParameters()
val currentUser = call.getUserSession()?.user ?: redirect("/login")
- val form = call.receiveParameters()
val newAdmiral = Admiral(
owningUser = currentUser,
}
post("/admiral/{id}/manage") {
+ val form = call.receiveValidatedParameters()
+
val currentUser = call.getUserSession()?.user
val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
val admiral = Admiral.get(admiralId)!!
if (admiral.owningUser != currentUser) throw ForbiddenException()
- val form = call.receiveParameters()
val newAdmiral = admiral.copy(
name = form["name"]?.takeIf { it.isNotBlank() } ?: admiral.name,
isFemale = form["sex"] == "female"
}
post("/admiral/{id}/rename/{ship}") {
+ val formParams = call.receiveValidatedParameters()
val currentUser = call.getUserSession()?.user
val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
if (admiral.owningUser != currentUser) throw ForbiddenException()
if (ship.owningAdmiral != admiralId) throw ForbiddenException()
- val newName = call.receiveParameters()["name"]?.takeIf { it.isNotBlank() && it.length <= SHIP_NAME_MAX_LENGTH } ?: redirect("/admiral/${admiralId}/manage")
+ val newName = formParams["name"]?.takeIf { it.isNotBlank() && it.length <= SHIP_NAME_MAX_LENGTH } ?: redirect("/admiral/${admiralId}/manage")
ShipInDrydock.set(shipId, setValue(ShipInDrydock::name, newName))
redirect("/admiral/${admiralId}/manage")
}
}
post("/admiral/{id}/sell/{ship}") {
+ call.receiveValidatedParameters()
+
val currentUser = call.getUserSession()?.user
val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
}
post("/admiral/{id}/buy/{ship}") {
+ call.receiveValidatedParameters()
+
val currentUser = call.getUserSession()?.user
val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
val admiral = Admiral.get(admiralId)!!
}
post("/admiral/{id}/delete") {
+ call.receiveValidatedParameters()
+
val currentUser = call.getUserSession()?.user
val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
val admiral = Admiral.get(admiralId)!!
import io.ktor.application.*
import io.ktor.features.*
+import io.ktor.http.*
import io.ktor.request.*
import io.ktor.sessions.*
+import io.ktor.util.*
+import starshipfights.ForbiddenException
import starshipfights.data.Id
import starshipfights.data.auth.User
import starshipfights.data.auth.UserSession
+import starshipfights.data.createNonce
+import starshipfights.redirect
import java.time.Instant
import java.time.temporal.ChronoUnit
return Id(text)
}
}
+
+data class CsrfInput(val cookie: Id<UserSession>, val target: String)
+
+object CsrfProtector {
+ private val nonces = mutableMapOf<String, CsrfInput>()
+
+ const val csrfInputName = "csrf-token"
+
+ fun newNonce(token: Id<UserSession>, action: String): String {
+ return createNonce().also { nonces[it] = CsrfInput(token, action) }
+ }
+
+ fun verifyNonce(nonce: String, token: Id<UserSession>, action: String): Boolean {
+ return nonces.remove(nonce) == CsrfInput(token, action)
+ }
+}
+
+suspend fun ApplicationCall.receiveValidatedParameters(): Parameters {
+ val formInput = receiveParameters()
+ val sessionId = sessions.get<Id<UserSession>>() ?: redirect("/login")
+ val csrfToken = formInput.getOrFail(CsrfProtector.csrfInputName)
+
+ if (CsrfProtector.verifyNonce(csrfToken, sessionId, request.uri))
+ return formInput
+ else
+ throw ForbiddenException()
+}
package starshipfights.info
import kotlinx.html.A
+import kotlinx.html.FORM
+import kotlinx.html.hiddenInput
+import starshipfights.auth.CsrfProtector
+import starshipfights.data.Id
+import starshipfights.data.auth.UserSession
var A.method: String?
get() = attributes["data-method"]
else
attributes.remove("data-method")
}
+
+fun FORM.csrfToken(cookie: Id<UserSession>) = hiddenInput {
+ name = CsrfProtector.csrfInputName
+ value = CsrfProtector.newNonce(cookie, formAction)
+}
section {
h1 { +"User Preferences" }
form(method = FormMethod.post, action = "/me/manage") {
+ csrfToken(currentSession.id)
h2 {
+"Profile"
}
}
suspend fun ApplicationCall.createAdmiralPage(): HTML.() -> Unit {
- getUser() ?: redirect("/login")
+ val sessionId = getUserSession()?.id ?: redirect("/login")
return page(
"Creating Admiral", standardNavBar(), null
section {
h1 { +"Creating Admiral" }
form(method = FormMethod.post, action = "/admiral/new") {
+ csrfToken(sessionId)
h3 {
label {
htmlFor = "faction"
}
suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
- val currentUser = getUserSession()?.user
+ val currentSession = getUserSession() ?: redirect("/login")
+ val currentUser = currentSession.user
+
val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
val admiral = Admiral.get(admiralId)!!
section {
h1 { +"Managing ${admiral.name}" }
form(method = FormMethod.post, action = "/admiral/${admiral.id}/manage") {
+ csrfToken(currentSession.id)
h3 {
label {
htmlFor = "name"
}
suspend fun ApplicationCall.renameShipPage(): HTML.() -> Unit {
- val currentUser = getUserSession()?.user
+ val currentSession = getUserSession() ?: redirect("/login")
+ val currentUser = currentSession.user
val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
val shipId = parameters["ship"]?.let { Id<ShipInDrydock>(it) }!!
+"${admiral.fullName} is about to rename the ${ship.shipType.fullDisplayName} ${ship.shipData.fullName}. Choose a name here:"
}
form(method = FormMethod.post, action = "/admiral/${admiral.id}/rename/${ship.id}") {
+ csrfToken(currentSession.id)
textInput(name = "name") {
id = "name"
value = ship.name
}
suspend fun ApplicationCall.sellShipConfirmPage(): HTML.() -> Unit {
- val currentUser = getUserSession()?.user
+ val currentSession = getUserSession() ?: redirect("/login")
+ val currentUser = currentSession.user
val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
val shipId = parameters["ship"]?.let { Id<ShipInDrydock>(it) }!!
}
}
form(method = FormMethod.post, action = "/admiral/${admiral.id}/sell/${ship.id}") {
+ csrfToken(currentSession.id)
submitInput {
value = "Sell"
}
}
suspend fun ApplicationCall.buyShipConfirmPage(): HTML.() -> Unit {
- val currentUser = getUserSession()?.user
+ val currentSession = getUserSession() ?: redirect("/login")
+ val currentUser = currentSession.user
+
val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
val admiral = Admiral.get(admiralId)!!
}
}
form(method = FormMethod.post, action = "/admiral/${admiral.id}/buy/${shipType.toUrlSlug()}") {
+ csrfToken(currentSession.id)
submitInput {
value = "Checkout"
}
}
suspend fun ApplicationCall.deleteAdmiralConfirmPage(): HTML.() -> Unit {
- val currentUser = getUserSession()?.user
+ val currentSession = getUserSession() ?: redirect("/login")
+ val currentUser = currentSession.user
+
val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
val admiral = Admiral.get(admiralId)!!
}
}
form(method = FormMethod.post, action = "/admiral/${admiral.id}/delete") {
+ csrfToken(currentSession.id)
submitInput(classes = "evil") {
value = "Yes"
}