diff --git a/.gitignore b/.gitignore index 736c56d..384a7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -.gradle/ +*gradle* build/ kodeshare.log +.idea/ diff --git a/build.gradle.kts b/build.gradle.kts index 15abfd8..b917fc6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") version "1.3.50" + id("com.github.johnrengelman.shadow") version "5.1.0" apply true application } @@ -30,6 +31,7 @@ dependencies { implementation("org.jetbrains.exposed:exposed:0.17.3") implementation("org.jetbrains:kotlin-css-jvm:1.0.0-pre.83-kotlin-1.3.50") implementation("org.mariadb.jdbc:mariadb-java-client:2.4.4") + implementation("com.uchuhimo:konf-core:0.20.0") } tasks.withType { diff --git a/kodeshare.properties b/kodeshare.properties new file mode 100644 index 0000000..b163291 --- /dev/null +++ b/kodeshare.properties @@ -0,0 +1 @@ +database.password=12345 diff --git a/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt b/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt index 0b7fe21..19c56c5 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Kodeshare.kt @@ -8,6 +8,7 @@ import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import moe.kageru.kodeshare.Routes.createRoutes +import org.joda.time.DateTime @KtorExperimentalLocationsAPI @ExperimentalStdlibApi @@ -21,4 +22,4 @@ fun main() { }.start(wait = true) } -data class Paste(val content: String, val html: String?) \ No newline at end of file +data class Paste(val content: String, val created: DateTime) \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kodeshare/Routes.kt b/src/main/kotlin/moe/kageru/kodeshare/Routes.kt index 7476fc3..c351c04 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/Routes.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/Routes.kt @@ -5,29 +5,33 @@ 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.MultiPartData.Empty.readPart import io.ktor.http.content.PartData import io.ktor.http.content.forEachPart +import io.ktor.http.content.readAllParts import io.ktor.http.content.streamProvider import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.get +import io.ktor.request.receive import io.ktor.request.receiveMultipart import io.ktor.response.respond -import io.ktor.response.respondRedirect import io.ktor.response.respondText import io.ktor.routing.Routing import io.ktor.routing.get import io.ktor.routing.post 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(Homepage.content) + call.respond(HttpStatusCode.OK, Homepage.content) } get("/style.css") { call.respondText(Css.default, ContentType.Text.CSS) @@ -38,6 +42,9 @@ object Routes { get { req -> call.handleGet(req) } + get { req -> + call.handleRaw(req) + } } @ExperimentalStdlibApi @@ -48,13 +55,13 @@ object Routes { val content = part.streamProvider().use { it.readBytes().decodeToString() } respondToUpload(content)?.let { id -> Log.info("Saving new file paste with ID $id") - } ?: Log.info("Invalid upload: $content") + } } is PartData.FormItem -> { val content = part.value respondToUpload(content)?.let { id -> Log.info("Saving new text paste with ID $id") - } ?: Log.info("Invalid upload: $content") + } } is PartData.BinaryItem -> { Log.warn("Received binary item from upload form. This shouldn’t happen.") @@ -65,10 +72,16 @@ object Routes { private suspend fun ApplicationCall.respondToUpload(content: String): Long? { content.ifBlank { - respondText("Empty pastes are not allowed", ContentType.Text.Any, HttpStatusCode.BadRequest) + Log.info("Rejecting blank paste") + respond(HttpStatusCode.BadRequest, "Empty pastes are not allowed") + return null + } + if (content.length > 1 * 1024 * 1024) { + Log.info("Rejecting paste over 1MB") + respond(HttpStatusCode.BadRequest, "Pastes are limited to 1MB each") return null } - val id = PasteDao.insert(Paste(content, null)).id + val id = PasteDao.insert(Paste(content, DateTime.now())).id // 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. @@ -81,11 +94,26 @@ object Routes { val id = req.id Log.info("Retrieving paste $id") PasteDao.select(id)?.data?.let { paste -> - respondText(paste.html ?: paste.content, ContentType.Text.Plain) + respond( + HttpStatusCode.OK, + PastePage.build(paste.content) + ) + } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") + } + + private suspend fun ApplicationCall.handleRaw(req: RawPasteRequest) { + val id = req.id + Log.info("Retrieving raw paste $id") + PasteDao.select(id)?.data?.let { paste -> + respondText(paste.content, ContentType.Text.Plain) } ?: respond(HttpStatusCode.NotFound, "nothing found for id $id") } } +@KtorExperimentalLocationsAPI +@Location("/r/{id}") +data class RawPasteRequest(val id: Long) + @KtorExperimentalLocationsAPI @Location("/{id}") data class PasteRequest(val id: Long) diff --git a/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt b/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt new file mode 100644 index 0000000..dceebfe --- /dev/null +++ b/src/main/kotlin/moe/kageru/kodeshare/config/Config.kt @@ -0,0 +1,15 @@ +package moe.kageru.kodeshare.config + +import com.uchuhimo.konf.Config +import com.uchuhimo.konf.ConfigSpec + +val config = Config { addSpec(DatabaseSpec) } + .from.properties.file("kodeshare.properties") + .from.env() + +object DatabaseSpec : ConfigSpec() { + val port by optional(3306) + val password by required() + val user by optional("kodeshare") + val database by optional("kode") +} \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt index ac1a18b..64b1898 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/Css.kt @@ -5,21 +5,22 @@ import kotlinx.css.properties.ms import kotlinx.css.properties.transition object Css { - private val accent1 = Color("#cd7400") - private val accent2 = Color("#ed7a00") + private val accent1 = Color("#e6db74") + private val accent2 = Color("#a6e22e") private val fontcolor = Color.lightGrey + private val bgcolor = Color("#23241f") val default = CSSBuilder().apply { body { fontFamily = "Hack, Fira Code, Noto Mono, monospace" fontSize = 13.pt textAlign = TextAlign.center margin = "auto" - backgroundColor = Color.black + backgroundColor = bgcolor color = fontcolor } textarea { fontFamily = "Hack, Fira Code, Noto Mono, monospace" - backgroundColor = Color.black + backgroundColor = bgcolor color = Color.white fontSize = 13.pt borderColor = accent1 @@ -30,6 +31,14 @@ object Css { minWidth = 70.pct maxWidth = 100.pct } + // this doesn’t inherit the style from anything else for some reason + rule(".hljs") { + width = 100.pct + height = 100.pct + textAlign = TextAlign.left + fontFamily = "Hack, Fira Code, Noto Mono, monospace" + fontSize = 13.pt + } rule("input[type=\"submit\"]") { backgroundColor = accent1 borderColor = accent1 @@ -44,7 +53,8 @@ object Css { } rule("input[type=\"submit\"]:hover") { backgroundColor = Color.transparent - color = accent1 + borderColor = accent2 + color = accent2 } rule("textarea:focus") { borderColor = accent2 diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt index 343bbc6..060000c 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/Homepage.kt @@ -8,6 +8,9 @@ object Homepage { val content = HtmlContent(HttpStatusCode.OK) { head { link(rel = "stylesheet", href = "/style.css", type = "text/css") + unsafe { + +"" + } } body { h1 { +"kodeshare - yet another paste service" } diff --git a/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt b/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt new file mode 100644 index 0000000..00e45e9 --- /dev/null +++ b/src/main/kotlin/moe/kageru/kodeshare/pages/PastePage.kt @@ -0,0 +1,27 @@ +package moe.kageru.kodeshare.pages + +import io.ktor.html.HtmlContent +import io.ktor.http.HttpStatusCode +import kotlinx.html.* + +object PastePage { + fun build(content: String) = HtmlContent(HttpStatusCode.OK) { + head { + link(rel = "stylesheet", href = "/style.css", type = "text/css") + link( + rel = "stylesheet", + href = "https://p.kageru.moe/static/hljs.css", + type = "text/css" + ) + script(src = "https://p.kageru.moe/static/hl.js") {} + unsafe { +"" } + } + body { + pre { + code { + +content + } + } + } + } +} diff --git a/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt b/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt index 332d89b..c4b7e10 100644 --- a/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt +++ b/src/main/kotlin/moe/kageru/kodeshare/persistence/PasteDao.kt @@ -1,6 +1,8 @@ package moe.kageru.kodeshare.persistence import moe.kageru.kodeshare.Paste +import moe.kageru.kodeshare.config.DatabaseSpec +import moe.kageru.kodeshare.config.config import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction import org.mariadb.jdbc.MariaDbDataSource @@ -13,20 +15,20 @@ object PasteDao { fun insert(paste: Paste): Transient = transaction { val id = PasteTable.insert { it[content] = paste.content - it[html] = paste.html + it[created] = paste.created }[PasteTable.id] Transient(id, paste) } init { val source = MariaDbDataSource().apply { - userName = "kodeshare" - setPassword("12345") - databaseName = "kode" + userName = config[DatabaseSpec.user] + setPassword(config[DatabaseSpec.password]) + databaseName = config[DatabaseSpec.database] + port = config[DatabaseSpec.port] } Database.connect(source) transaction { - SchemaUtils.drop(PasteTable) SchemaUtils.create(PasteTable) } } @@ -36,14 +38,14 @@ private fun ResultRow.toPaste() = Transient( get(PasteTable.id), Paste( content = get(PasteTable.content), - html = get(PasteTable.html) + created = get(PasteTable.created) ) ) private object PasteTable : Table() { - val id = long("id").primaryKey().autoIncrement() + val id = long("id").primaryKey().autoIncrement().index() val content = text("content") - val html = text("html").nullable() + val created = datetime("created").index() } /*