Skip to content

Commit

Permalink
implement basic auth
Browse files Browse the repository at this point in the history
  • Loading branch information
powercasgamer committed Nov 12, 2023
1 parent 6adf478 commit 58786b7
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 12 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# imagery

A WIP ShareX file server.
A WIP ShareX file server.

Overcomplicating? Never heard of her.
38 changes: 32 additions & 6 deletions app/src/main/kotlin/dev/mizule/imagery/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String, FileCacheEntry> = Caffeine.newBuilder()
private val cache: Cache<String, FileCacheEntry> = Caffeine.newBuilder() // This is really not needed, but, yes.
.expireAfterWrite(15, TimeUnit.MINUTES)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build()
Expand All @@ -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
}
Expand All @@ -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) {
Expand All @@ -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(),
Expand Down Expand Up @@ -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() {
Expand All @@ -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("")
}

Expand Down
68 changes: 68 additions & 0 deletions app/src/main/kotlin/dev/mizule/imagery/app/auth/AuthHandler.kt
Original file line number Diff line number Diff line change
@@ -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<String, User> = 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<UserConfig>()) {
"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
}
}
11 changes: 11 additions & 0 deletions app/src/main/kotlin/dev/mizule/imagery/app/auth/User.kt
Original file line number Diff line number Diff line change
@@ -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,
)
10 changes: 10 additions & 0 deletions app/src/main/kotlin/dev/mizule/imagery/app/auth/UserConfig.kt
Original file line number Diff line number Diff line change
@@ -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<User> = mutableListOf()
)
3 changes: 2 additions & 1 deletion app/src/main/kotlin/dev/mizule/imagery/app/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -40,4 +41,4 @@ data class Config(

@Comment("The path to the uploaded file storage directory.")
val storagePath: String = "./storage",
)
)
11 changes: 7 additions & 4 deletions app/src/main/kotlin/dev/mizule/imagery/app/launcher/Launcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,27 @@
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
import kotlin.io.path.exists

fun main(args: Array<String>) {
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 ->
Expand All @@ -65,7 +68,7 @@ fun main(args: Array<String>) {
configLoader.save(configNode)
}

val app = App(config)
val app = App(config, usersPathOption)
Runtime.getRuntime().addShutdownHook(Thread(app::stop))

app.start()
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/kotlin/dev/mizule/imagery/app/model/Roles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.mizule.imagery.app.model

import io.javalin.security.RouteRole

enum class Roles : RouteRole {

PUBLIC,
PRIVATE
}

0 comments on commit 58786b7

Please sign in to comment.