kodeshare/src/main/kotlin/moe/kageru/kodeshare/Routes.kt

152 lines
5.2 KiB
Kotlin
Raw Normal View History

2019-09-19 22:04:23 +02:00
package moe.kageru.kodeshare
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.ContentType
2019-09-22 18:26:28 +02:00
import io.ktor.http.HttpHeaders
2019-09-19 22:04:23 +02:00
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.http.content.streamProvider
2019-09-22 18:26:28 +02:00
import io.ktor.locations.KtorExperimentalLocationsAPI
2019-09-19 22:04:23 +02:00
import io.ktor.locations.Location
import io.ktor.locations.get
2019-09-29 16:20:50 +02:00
import io.ktor.locations.head
2019-09-19 22:04:23 +02:00
import io.ktor.request.receiveMultipart
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.Routing
import io.ktor.routing.get
2019-09-29 16:20:50 +02:00
import io.ktor.routing.head
2019-09-19 22:04:23 +02:00
import io.ktor.routing.post
2019-09-29 11:50:14 +02:00
import moe.kageru.kodeshare.config.ServerSpec
import moe.kageru.kodeshare.config.config
import moe.kageru.kodeshare.pages.AboutPage
2019-09-22 18:26:28 +02:00
import moe.kageru.kodeshare.pages.Css
import moe.kageru.kodeshare.pages.Homepage
2019-09-29 08:17:31 +02:00
import moe.kageru.kodeshare.pages.PastePage
2019-09-19 22:04:23 +02:00
import moe.kageru.kodeshare.persistence.PasteDao
2019-09-29 08:17:31 +02:00
import org.joda.time.DateTime
2019-09-19 22:04:23 +02:00
2019-09-22 18:26:28 +02:00
@KtorExperimentalLocationsAPI
2019-09-19 22:04:23 +02:00
@ExperimentalStdlibApi
object Routes {
fun Routing.createRoutes() {
get("/") {
2019-09-29 08:17:31 +02:00
call.respond(HttpStatusCode.OK, Homepage.content)
2019-09-22 18:26:28 +02:00
}
get("/style.css") {
call.respondText(Css.default, ContentType.Text.CSS)
2019-09-19 22:04:23 +02:00
}
post("/") {
2019-09-22 18:26:28 +02:00
call.handlePost()
2019-09-19 22:04:23 +02:00
}
2019-09-29 10:15:42 +02:00
get("/favicon.ico") {
call.respond(HttpStatusCode.NotFound)
}
2019-09-29 11:50:14 +02:00
get("/about") {
call.respond(HttpStatusCode.OK, AboutPage.content)
}
2019-09-29 17:03:44 +02:00
get<HtmlPasteRequest> { req ->
2019-09-29 17:10:02 +02:00
call.handleGet(req, raw = false)
2019-09-19 22:04:23 +02:00
}
2019-09-29 08:17:31 +02:00
get<RawPasteRequest> { req ->
2019-09-29 17:10:02 +02:00
call.handleGet(req, raw = true)
2019-09-29 08:17:31 +02:00
}
2019-09-29 16:20:50 +02:00
head("/") {
call.respond(HttpStatusCode.OK)
}
head<RawPasteRequest> { req ->
2019-09-29 17:03:44 +02:00
call.handleHead(req, true)
2019-09-29 16:20:50 +02:00
}
2019-09-29 17:03:44 +02:00
head<HtmlPasteRequest> { req ->
2019-09-29 16:20:50 +02:00
call.handleHead(req)
}
}
2019-09-29 17:03:44 +02:00
private fun splitPath(uri: String): Pair<String, String?> {
return if (uri.contains('.')) {
val (name, ext) = uri.split(".", limit = 2)
Pair(name, ext)
} else {
Pair(uri, null)
}
}
2019-09-29 17:10:02 +02:00
private suspend fun ApplicationCall.handleHead(paste: PasteRequest, raw: Boolean = false) {
val uri = splitPath(paste.uri).first
if (PasteDao.selectByUri(uri) != null) {
respond(HttpStatusCode.OK, if (raw) ContentType.Text.Plain else ContentType.Text.Html)
} else {
respond(HttpStatusCode.NotFound)
}
}
2019-09-22 18:26:28 +02:00
@ExperimentalStdlibApi
private suspend fun ApplicationCall.handlePost() {
receiveMultipart().forEachPart { part ->
when (part) {
is PartData.FileItem -> {
val content = part.streamProvider().use { it.readBytes().decodeToString() }
2019-09-29 17:10:02 +02:00
processUpload(content)?.let { uri ->
2019-09-29 09:51:15 +02:00
Log.info("Saving new file paste with uri $uri")
2019-09-29 08:17:31 +02:00
}
2019-09-22 18:26:28 +02:00
}
is PartData.FormItem -> {
val content = part.value
2019-09-29 17:10:02 +02:00
processUpload(content)?.let { uri ->
2019-09-29 09:51:15 +02:00
Log.info("Saving new text paste with uri $uri")
2019-09-29 08:17:31 +02:00
}
2019-09-22 18:26:28 +02:00
}
is PartData.BinaryItem -> {
Log.warn("Received binary item from upload form. This shouldn’t happen.")
}
2019-09-19 22:04:23 +02:00
}
}
}
2019-09-29 17:10:02 +02:00
private suspend fun ApplicationCall.processUpload(content: String): String? {
2019-09-22 18:26:28 +02:00
content.ifBlank {
2019-09-29 08:17:31 +02:00
Log.info("Rejecting blank paste")
respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed")
return null
}
if (content.length > 1024 * 1024) {
Log.info("Rejecting paste over 1M")
respond(HttpStatusCode.BadRequest, "Pastes are limited to 1 MiB")
2019-09-22 18:26:28 +02:00
return null
}
2019-09-29 09:51:15 +02:00
val uri = PasteDao.insert(Paste(content, DateTime.now(), Paste.randomUri())).data.uri
2019-09-22 18:26:28 +02:00
// This will show the URL in a terminal when uploading via curl,
// while also redirecting browser uploads to the newly created paste.
// May seem odd to return code 302, but it seems to be the only way.
2019-09-29 09:51:15 +02:00
response.headers.append(HttpHeaders.Location, uri)
2020-10-28 12:23:21 +01:00
respond(HttpStatusCode.Found, "${config[ServerSpec.pasteurl].ifBlank { config[ServerSpec.domain] }}$uri\n")
2019-09-29 09:51:15 +02:00
return uri
2019-09-22 18:26:28 +02:00
}
2019-09-29 17:10:02 +02:00
private suspend fun ApplicationCall.handleGet(req: PasteRequest, raw: Boolean) {
2019-09-29 17:03:44 +02:00
val (uri, ext) = splitPath(req.uri)
2019-09-29 09:51:15 +02:00
Log.info("Retrieving paste $uri")
PasteDao.selectByUri(uri)?.data?.let { paste ->
2019-09-29 17:10:02 +02:00
if (raw) {
respondText(paste.content, ContentType.Text.Plain)
} else {
2020-03-12 10:57:28 +01:00
respond(HttpStatusCode.OK, PastePage.build(paste.content, paste.uri, ext))
2019-09-29 17:10:02 +02:00
}
2020-10-28 12:23:21 +01:00
} ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri\n")
2019-09-22 18:26:28 +02:00
}
2019-09-19 22:04:23 +02:00
}
2019-09-29 09:51:15 +02:00
@KtorExperimentalLocationsAPI
@Location("/r/{uri}")
2019-09-29 17:03:44 +02:00
data class RawPasteRequest(override val uri: String) : PasteRequest
2019-09-29 08:17:31 +02:00
2019-09-22 18:26:28 +02:00
@KtorExperimentalLocationsAPI
2019-09-29 09:51:15 +02:00
@Location("/{uri}")
2019-09-29 17:03:44 +02:00
data class HtmlPasteRequest(override val uri: String) : PasteRequest
2019-09-29 09:51:15 +02:00
2019-09-29 17:03:44 +02:00
interface PasteRequest {
val uri: String
2019-09-29 09:51:15 +02:00
}