diff --git a/README.md b/README.md index f000a28..c88d1b9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # imagery -A WIP ShareX file server. \ No newline at end of file +A WIP ShareX file server. + +Overcomplicating? Never heard of her. \ No newline at end of file diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/App.kt b/app/src/main/kotlin/dev/mizule/imagery/app/App.kt index 37fe5c6..11b22bf 100644 --- a/app/src/main/kotlin/dev/mizule/imagery/app/App.kt +++ b/app/src/main/kotlin/dev/mizule/imagery/app/App.kt @@ -27,8 +27,11 @@ package dev.mizule.imagery.app import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.benmanes.caffeine.cache.Cache import com.github.benmanes.caffeine.cache.Caffeine +import dev.mizule.imagery.app.auth.AuthHandler +import dev.mizule.imagery.app.auth.UserConfig import dev.mizule.imagery.app.config.Config import dev.mizule.imagery.app.model.ImageLookupResult +import dev.mizule.imagery.app.model.Roles import dev.mizule.imagery.app.model.UploadedFile import io.github.oshai.kotlinlogging.KotlinLogging import io.javalin.Javalin @@ -48,19 +51,21 @@ import kotlin.io.path.outputStream private val logger = KotlinLogging.logger {} -class App(private val config: Config) { +class App(private val config: Config, usersConfigOption: String) { private val storageDir = Path(config.storagePath) private val dataLoader = JacksonConfigurationLoader.builder() .path(Path(config.indexPath)) .defaultOptions { options -> + options.shouldCopyDefaults(true) options.serializers { builder -> builder.registerAnnotatedObjects(objectMapperFactory()) } } .build() + private val authHandler = AuthHandler(usersConfigOption) private val dataNode = dataLoader.load() - private val cache: Cache = Caffeine.newBuilder() + private val cache: Cache = Caffeine.newBuilder() // This is really not needed, but, yes. .expireAfterWrite(15, TimeUnit.MINUTES) .expireAfterAccess(10, TimeUnit.MINUTES) .build() @@ -69,6 +74,7 @@ class App(private val config: Config) { it.jsonMapper(JavalinJackson(MAPPER)) it.showJavalinBanner = false it.router.ignoreTrailingSlashes = true + it.useVirtualThreads = true it.contextResolver.ip = { ctx -> ctx.header("CF-Connecting-IP") ?: ctx.req().remoteAddr } @@ -81,7 +87,25 @@ class App(private val config: Config) { logger.info { "Received ${ctx.method()} request from: ${ctx.ip()}:${ctx.port()} for ${ctx.fullUrl()}" } } javalin.get("/{id}", ::serveUploadedFile) - javalin.post("/upload", ::handleFileUpload) + if (authHandler.usersConfig.users.isEmpty()) { + authHandler.createUser("powercas_gamer") + } + javalin.beforeMatched("/upload") { ctx -> + if (ctx.routeRoles().contains(Roles.PRIVATE)) { + // check auth header + val token = ctx.header("Authorization") ?: throw io.javalin.http.ForbiddenResponse() + logger.info { "Someone tried to use the following token: $token" } + if (!authHandler.isAuthorized(token)) { + logger.info { "Token: $token Unauthorized" } + throw io.javalin.http.ForbiddenResponse() + } else { + logger.info { "Token: $token Authorized" } + + } + + } + } + javalin.post("/upload", ::handleFileUpload, Roles.PRIVATE) } private fun handleFileUpload(ctx: Context) { @@ -97,10 +121,12 @@ class App(private val config: Config) { filePath.outputStream().use { file.content().copyTo(it) } + val token = ctx.header("Authorization") ?: throw io.javalin.http.ForbiddenResponse() + val uploadedFile = UploadedFile( id, - "user", + authHandler.getUserByToken(token)?.username ?: "Unknown", System.currentTimeMillis(), fileName, file.filename(), @@ -130,7 +156,7 @@ class App(private val config: Config) { }?.also { (uploadedFile, path) -> ctx.result(path.inputStream()) .contentType(ContentType.getContentTypeByExtension(uploadedFile.extension) ?: ContentType.IMAGE_PNG) - }?: ctx.result("This image does not exists").status(HttpStatus.NOT_FOUND) + } ?: ctx.result("This image does not exists").status(HttpStatus.NOT_FOUND) } fun start() { @@ -146,7 +172,7 @@ class App(private val config: Config) { private val MAPPER = jacksonObjectMapper() private val ALLOWED_CHARS = ('A'..'Z') + ('a'..'z') + ('0'..'9') - private fun getRandomString(length: Int = 8): String = + fun getRandomString(length: Int = 8): String = generateSequence(ALLOWED_CHARS::random).take(length).joinToString("") } diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/auth/AuthHandler.kt b/app/src/main/kotlin/dev/mizule/imagery/app/auth/AuthHandler.kt new file mode 100644 index 0000000..f778f54 --- /dev/null +++ b/app/src/main/kotlin/dev/mizule/imagery/app/auth/AuthHandler.kt @@ -0,0 +1,68 @@ +package dev.mizule.imagery.app.auth + +import org.spongepowered.configurate.jackson.JacksonConfigurationLoader +import org.spongepowered.configurate.kotlin.extensions.get +import org.spongepowered.configurate.kotlin.objectMapperFactory +import java.security.SecureRandom +import java.util.Base64 +import java.util.HashMap +import kotlin.io.path.Path +import kotlin.io.path.exists + +class AuthHandler(userConfigPath: String) { + + private val usersMap: MutableMap = mutableMapOf() + + val usersPath = Path(userConfigPath) + val usersLoader = JacksonConfigurationLoader.builder() + .path(usersPath) + .defaultOptions { options -> + options.shouldCopyDefaults(true) + options.serializers { builder -> + builder.registerAnnotatedObjects(objectMapperFactory()) + } + } + .build() + + val usersNode = usersLoader.load() + val usersConfig = requireNotNull(usersNode.get()) { + "Could not read user configuration" + } + + init { + if (!usersPath.exists()) { + usersNode.set(usersConfig) // update the backing node to add defaults + usersLoader.save(usersNode) + } + loadUsers() + } + + private fun loadUsers() { + usersConfig.users.forEach { + this.usersMap[it.username] = User(it.username, it.token) + } + } + + fun createUser(name: String): User { + val secret = ByteArray(48) + SecureRandom().nextBytes(secret) + return createUser(name, Base64.getEncoder().encodeToString(secret)); + } + + fun isAuthorized(token: String): Boolean { + return usersMap.values.any { it.token == token } + } + + fun getUserByToken(token: String): User? { + return usersMap.values.find { it.token == token } + } + + fun createUser(name: String, token: String): User { + val user = User(name, token) + this.usersMap[name] to user + this.usersConfig.users.add(user) + usersNode.set(usersConfig) + usersLoader.save(usersNode) + return user + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/auth/User.kt b/app/src/main/kotlin/dev/mizule/imagery/app/auth/User.kt new file mode 100644 index 0000000..80085a4 --- /dev/null +++ b/app/src/main/kotlin/dev/mizule/imagery/app/auth/User.kt @@ -0,0 +1,11 @@ +package dev.mizule.imagery.app.auth + +import org.spongepowered.configurate.objectmapping.ConfigSerializable + +@ConfigSerializable +data class User( + + val username: String, + + val token: String, +) diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/auth/UserConfig.kt b/app/src/main/kotlin/dev/mizule/imagery/app/auth/UserConfig.kt new file mode 100644 index 0000000..95f5956 --- /dev/null +++ b/app/src/main/kotlin/dev/mizule/imagery/app/auth/UserConfig.kt @@ -0,0 +1,10 @@ +package dev.mizule.imagery.app.auth + +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import org.spongepowered.configurate.objectmapping.meta.Comment + +@ConfigSerializable +data class UserConfig( + + val users: MutableList = mutableListOf() +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/config/Config.kt b/app/src/main/kotlin/dev/mizule/imagery/app/config/Config.kt index 03bc228..50341c5 100644 --- a/app/src/main/kotlin/dev/mizule/imagery/app/config/Config.kt +++ b/app/src/main/kotlin/dev/mizule/imagery/app/config/Config.kt @@ -24,6 +24,7 @@ */ package dev.mizule.imagery.app.config +import dev.mizule.imagery.app.App import org.spongepowered.configurate.objectmapping.ConfigSerializable import org.spongepowered.configurate.objectmapping.meta.Comment @@ -40,4 +41,4 @@ data class Config( @Comment("The path to the uploaded file storage directory.") val storagePath: String = "./storage", -) +) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt b/app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt index 6d4fccd..1bb54f5 100644 --- a/app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt +++ b/app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt @@ -27,11 +27,13 @@ package dev.mizule.imagery.app.launcher import dev.mizule.imagery.app.App +import dev.mizule.imagery.app.auth.UserConfig import dev.mizule.imagery.app.config.Config import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.default import org.spongepowered.configurate.hocon.HoconConfigurationLoader +import org.spongepowered.configurate.jackson.JacksonConfigurationLoader import org.spongepowered.configurate.kotlin.extensions.get import org.spongepowered.configurate.kotlin.objectMapperFactory import kotlin.io.path.Path @@ -39,12 +41,13 @@ import kotlin.io.path.exists fun main(args: Array) { val parser = ArgParser("imagery") - val path by parser.option(ArgType.String, shortName = "p", description = "The configuration file path, created if not present.") + val configPathOption by parser.option(ArgType.String, shortName = "p", description = "The configuration file path, created if not present.") .default("./config.conf") - + val usersPathOption by parser.option(ArgType.String, shortName = "u", description = "The user configuration file path, created if not present.") + .default("./users.json") parser.parse(args) - val configPath = Path(path) + val configPath = Path(configPathOption) val configLoader = HoconConfigurationLoader.builder() .path(configPath) .defaultOptions { options -> @@ -65,7 +68,7 @@ fun main(args: Array) { configLoader.save(configNode) } - val app = App(config) + val app = App(config, usersPathOption) Runtime.getRuntime().addShutdownHook(Thread(app::stop)) app.start() diff --git a/app/src/main/kotlin/dev/mizule/imagery/app/model/Roles.kt b/app/src/main/kotlin/dev/mizule/imagery/app/model/Roles.kt new file mode 100644 index 0000000..aba845c --- /dev/null +++ b/app/src/main/kotlin/dev/mizule/imagery/app/model/Roles.kt @@ -0,0 +1,9 @@ +package dev.mizule.imagery.app.model + +import io.javalin.security.RouteRole + +enum class Roles : RouteRole { + + PUBLIC, + PRIVATE +} \ No newline at end of file