package moe.kageru.kodeshare import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.http.ContentType import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart import io.ktor.http.content.streamProvider import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.locations.head 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 import io.ktor.routing.head import io.ktor.routing.post import moe.kageru.kodeshare.config.ServerSpec import moe.kageru.kodeshare.config.config import moe.kageru.kodeshare.pages.AboutPage import moe.kageru.kodeshare.pages.Css import moe.kageru.kodeshare.pages.Homepage import moe.kageru.kodeshare.pages.PastePage import moe.kageru.kodeshare.persistence.PasteDao import org.joda.time.DateTime @KtorExperimentalLocationsAPI @ExperimentalStdlibApi object Routes { fun Routing.createRoutes() { get("/") { call.respond(HttpStatusCode.OK, Homepage.content) } get("/style.css") { call.respondText(Css.default, ContentType.Text.CSS) } post("/") { call.handlePost() } get("/favicon.ico") { call.respond(HttpStatusCode.NotFound) } get("/about") { call.respond(HttpStatusCode.OK, AboutPage.content) } get { req -> call.handleGet(req, raw = false) } get { req -> call.handleGet(req, raw = true) } head("/") { call.respond(HttpStatusCode.OK) } head { req -> call.handleHead(req, true) } head { req -> call.handleHead(req) } } private fun splitPath(uri: String): Pair { return if (uri.contains('.')) { val (name, ext) = uri.split(".", limit = 2) Pair(name, ext) } else { Pair(uri, null) } } 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) } } @ExperimentalStdlibApi private suspend fun ApplicationCall.handlePost() { receiveMultipart().forEachPart { part -> when (part) { is PartData.FileItem -> { val content = part.streamProvider().use { it.readBytes().decodeToString() } processUpload(content)?.let { uri -> Log.info("Saving new file paste with uri $uri") } } is PartData.FormItem -> { val content = part.value processUpload(content)?.let { uri -> Log.info("Saving new text paste with uri $uri") } } is PartData.BinaryItem -> { Log.warn("Received binary item from upload form. This shouldn’t happen.") } } } } private suspend fun ApplicationCall.processUpload(content: String): String? { content.ifBlank { 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") return null } val uri = PasteDao.insert(Paste(content, DateTime.now(), Paste.randomUri())).data.uri // 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. response.headers.append(HttpHeaders.Location, uri) respond(HttpStatusCode.Found, "${config[ServerSpec.pasteurl].ifBlank { config[ServerSpec.domain] }}$uri\n") return uri } private suspend fun ApplicationCall.handleGet(req: PasteRequest, raw: Boolean) { val (uri, ext) = splitPath(req.uri) Log.info("Retrieving paste $uri") PasteDao.selectByUri(uri)?.data?.let { paste -> if (raw) { respondText(paste.content, ContentType.Text.Plain) } else { respond(HttpStatusCode.OK, PastePage.build(paste.content, paste.uri, ext)) } } ?: respond(HttpStatusCode.NotFound, "nothing found for id $uri\n") } } @KtorExperimentalLocationsAPI @Location("/r/{uri}") data class RawPasteRequest(override val uri: String) : PasteRequest @KtorExperimentalLocationsAPI @Location("/{uri}") data class HtmlPasteRequest(override val uri: String) : PasteRequest interface PasteRequest { val uri: String }