diff options
Diffstat (limited to 'src/main/kotlin')
-rw-r--r-- | src/main/kotlin/App.kt | 241 | ||||
-rw-r--r-- | src/main/kotlin/CryptoHandler.kt | 51 | ||||
-rw-r--r-- | src/main/kotlin/DatabaseController.kt | 511 | ||||
-rw-r--r-- | src/main/kotlin/FileController.kt | 277 | ||||
-rw-r--r-- | src/main/kotlin/LogFilter.kt | 28 | ||||
-rw-r--r-- | src/main/kotlin/UserHandler.kt | 222 |
6 files changed, 0 insertions, 1330 deletions
diff --git a/src/main/kotlin/App.kt b/src/main/kotlin/App.kt deleted file mode 100644 index 55058be..0000000 --- a/src/main/kotlin/App.kt +++ /dev/null @@ -1,241 +0,0 @@ -package space.anity - -import com.fizzed.rocker.* -import com.fizzed.rocker.runtime.* -import io.javalin.* -import io.javalin.apibuilder.ApiBuilder.* -import io.javalin.rendering.* -import io.javalin.rendering.template.TemplateUtil.model -import io.javalin.security.* -import io.javalin.security.SecurityUtil.roles -import org.slf4j.* -import java.net.* -import kotlin.system.* - -const val debug = false -var silent = true -var port = 7000 -// TODO: Add abstract and secure file home support for windows/BSD/macOS -val fileHome = if (System.getProperty("os.name") != "Linux" || debug) "files" else "/usr/share/kloud/files" -val databaseController = DatabaseController() -val userHandler = UserHandler() -val fileController = FileController() -private val log = LoggerFactory.getLogger("App.kt") - -fun main(args: Array<String>) { - val app = startServer(args) - log.info("Successfully started server on port $port") - - // Set up templating - RockerRuntime.getInstance().isReloading = false - JavalinRenderer.register( - FileRenderer { filepath, model -> Rocker.template(filepath).bind(model).render().toString() }, ".rocker.html" - ) - - databaseController.initDatabase() - - app.routes { - /** - * Normalizes and cleans the requested url - */ - before("/*") { ctx -> - if (URI(ctx.url()).normalize().toString() != ctx.url()) ctx.redirect(URI(ctx.url()).normalize().toString()) - } - - /** - * Renders the static resources (important for deployed jar files) - */ - get( - "/css/*", { ctx -> - ctx.contentType("text/css") - try { - ctx.result(Thread.currentThread().contextClassLoader.getResourceAsStream("css/" + ctx.splat(0))) - } catch (_: Exception) { - throw NotFoundResponse() - } - }, - roles(Roles.GUEST, Roles.USER) - ) - get( - "/js/*", { ctx -> - ctx.contentType("text/javascript") - try { - ctx.result(Thread.currentThread().contextClassLoader.getResourceAsStream("js/" + ctx.splat(0))) - } catch (_: Exception) { - throw NotFoundResponse() - } - }, - roles(Roles.GUEST, Roles.USER) - ) - get( - "/fonts/*", { ctx -> - try { - ctx.result(Thread.currentThread().contextClassLoader.getResourceAsStream("fonts/" + ctx.splat(0))) - } catch (_: Exception) { - throw NotFoundResponse() - } - }, - roles(Roles.GUEST, Roles.USER) - ) - - /** - * Main page - */ - get("/", { ctx -> - ctx.render( - "index.rocker.html", - model("username", databaseController.getUsername(userHandler.getVerifiedUserId(ctx)), "ctx", ctx) - ) - }, roles(Roles.GUEST, Roles.USER)) - - /** - * Renders the login page - */ - get("/user/login", userHandler::renderLogin, roles(Roles.GUEST, Roles.USER)) - - /** - * Endpoint for user authentication - */ - post("/user/login", userHandler::login, roles(Roles.GUEST)) - - /** - * Logs the user out - */ - get("/user/logout", userHandler::logout, roles(Roles.USER)) - - /** - * Toggles the users theme - */ - post("/user/theme", userHandler::toggleTheme, roles(Roles.USER)) - - /** - * Renders the registration page - */ - get("/user/register", userHandler::renderRegistration, roles(Roles.GUEST)) - - /** - * Registers new user - */ - post("/user/register", userHandler::register, roles(Roles.GUEST)) - - /** - * Adds part of a new user (username) to database - */ - post("/user/add", databaseController::indexUserRegistration, roles(Roles.ADMIN)) - - /** - * Renders the admin interface - */ - get("/admin", userHandler::renderAdmin, roles(Roles.ADMIN)) - - /** - * Renders the setup page (only on initial use) - */ - get("/setup", userHandler::renderSetup, roles(Roles.GUEST)) - - /** - * Endpoint for setup (only on initial use) - */ - post("/setup", userHandler::setup, roles(Roles.GUEST)) - - /** - * Renders the file list view - */ - get("/files/*", fileController::crawl, roles(Roles.USER)) - - /** - * Receives and saves multipart media data - */ - post("/file/upload/*", fileController::upload, roles(Roles.USER)) - - /** - * Indexes every file of the user into the database - */ - get("/file/index", fileController::indexAll, roles(Roles.USER)) - - /** - * Deletes file - */ - post("/file/delete/*", fileController::delete, roles(Roles.USER)) - - /** - * Shares file - */ - post("/file/share/*", fileController::share, roles(Roles.USER)) - - /** - * Shares file in directory - */ - post("/file/share", fileController::handleSharedFile, roles(Roles.USER)) - - /** - * Shows the shared file - */ - get("/file/shared", fileController::renderShared, roles(Roles.GUEST, Roles.USER)) - } -} - -/** - * Sets up the roles with the database and declares the handling of roles - */ -fun roleManager(handler: Handler, ctx: Context, permittedRoles: Set<Role>) { - if (userHandler.getVerifiedUserId(ctx) == ctx.cookieStore("userId") ?: "userId" - && databaseController.getRoles(userHandler.getVerifiedUserId(ctx)).any { it in permittedRoles } - ) handler.handle(ctx) - else if (userHandler.getVerifiedUserId(ctx) != ctx.cookieStore("userId") ?: "userId" - && databaseController.getRoles(userHandler.getVerifiedUserId(ctx)).any { it in permittedRoles } - ) handler.handle(ctx) - else ctx.status(401).redirect("/user/login") -} - -/** - * Starts the server and parses the command line arguments - */ -fun startServer(args: Array<String>): Javalin { - var runServer = true - - args.forEachIndexed { index, element -> - run { - val wantsDebug = element.startsWith("-d") || element.startsWith("--debug") - val wantsPort = element.startsWith("-p") || element.startsWith("--port") - val wantsHelp = element.startsWith("-h") || element.startsWith("--help") - - if (wantsPort) { - val portArgument = args[index + 1].toInt() - if (portArgument in 1..65535) port = portArgument - } - if (wantsDebug) { - silent = false - } - if (wantsHelp) { - runServer = false - log.info( - "Help: \n" + - "Use -p or --port to specify a port\n" + - "Use -d or --debug to enable debug mode\n" + - "Use -h or --help to show this help message" - ) - } - } - } - - return if (runServer) { - try { - Javalin.create().apply { - port(port) - accessManager { handler, ctx, permittedRoles -> roleManager(handler, ctx, permittedRoles) } - disableStartupBanner() - }.start() - } catch (_: Exception) { - log.info("Port $port already in use!") - exitProcess(1) - } - } else exitProcess(1) -} - -/** - * Declares the roles in which a user can be in - */ -enum class Roles : Role { - ADMIN, USER, GUEST -} diff --git a/src/main/kotlin/CryptoHandler.kt b/src/main/kotlin/CryptoHandler.kt deleted file mode 100644 index e3f33df..0000000 --- a/src/main/kotlin/CryptoHandler.kt +++ /dev/null @@ -1,51 +0,0 @@ -package space.anity - -import java.io.* -import java.security.* -import javax.crypto.* -import javax.crypto.spec.* - -class CryptoHandler @Throws(NoSuchPaddingException::class, NoSuchAlgorithmException::class) -internal constructor(private val secretKey: SecretKey, cipher: String) { - private val cipher: Cipher = Cipher.getInstance(cipher) - - @Throws(InvalidKeyException::class, IOException::class) - internal fun encrypt(content: String, fileName: String) { - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - val iv = cipher.iv - - FileOutputStream(fileName).use { fileOut -> - fileOut.write(iv) - CipherOutputStream(fileOut, cipher).use { cipherOut -> - cipherOut.write(content.toByteArray()) - } - } - } - - @Throws(InvalidAlgorithmParameterException::class, InvalidKeyException::class, IOException::class) - internal fun decrypt(fileName: String): String { - var content = "" - - FileInputStream(fileName).use { fileIn -> - val iv = ByteArray(16) - fileIn.read(iv) - cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv)) - - CipherInputStream(fileIn, cipher).use { cipherIn -> - InputStreamReader(cipherIn, Charsets.UTF_8).use { inputReader -> - BufferedReader(inputReader).use { reader -> - val sb = StringBuilder() - var line: String? = reader.readLine() - while (line != null) { - sb.append(line + "\n") - line = reader.readLine() - } - content = sb.toString() - } - } - } - } - - return content // TODO: Fix char handling as 1 byte in decryption - } -} diff --git a/src/main/kotlin/DatabaseController.kt b/src/main/kotlin/DatabaseController.kt deleted file mode 100644 index 5316bb0..0000000 --- a/src/main/kotlin/DatabaseController.kt +++ /dev/null @@ -1,511 +0,0 @@ -package space.anity - -import at.favre.lib.crypto.bcrypt.* -import io.javalin.* -import io.javalin.rendering.template.TemplateUtil.model -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.* -import org.joda.time.* -import org.slf4j.* -import java.sql.* - -class DatabaseController { - private val dbFileLocation = - if (System.getProperty("os.name") != "Linux" || debug) "main.db" else "/usr/share/kloud/main.db" - val db: Database = Database.connect("jdbc:sqlite:$dbFileLocation", "org.sqlite.JDBC") - private val log = LoggerFactory.getLogger(this.javaClass.name) - - /** - * Database table indexing the file locations - */ - object FileLocation : Table() { - val id = integer("id").autoIncrement().primaryKey() - val path = text("path") - val isDirectory = bool("isDirectory").default(false) - val userId = integer("userId").references(UserData.id) - val accessId = varchar("accessId", 64).uniqueIndex() - val isShared = bool("isShared").default(false) - } - - /** - * Database table indexing the users with their regarding passwords - */ - object UserData : Table() { - val id = integer("id").autoIncrement().primaryKey() - val username = varchar("username", 24).uniqueIndex() - val password = varchar("password", 64) - val verification = varchar("verification", 64).uniqueIndex() - val darkTheme = bool("darkTheme").default(false) - } - - /** - * Database table indexing the users with their regarding role (multi line per user) - */ - object UserRoles : Table() { - val id = integer("id").autoIncrement().primaryKey() - val userId = integer("userId").references(UserData.id) - val roleId = integer("role").references(RolesData.id) - } - - /** - * Database table indexing the soon-to-be registered users by username - */ - object UserRegistration : Table() { - val id = integer("id").autoIncrement().primaryKey() - val username = varchar("username", 24).uniqueIndex() - val token = varchar("token", 64).uniqueIndex() - } - - /** - * Database table declaring available roles - */ - object RolesData : Table() { - val id = integer("id").autoIncrement().primaryKey() - val role = varchar("roles", 16) - } - - /** - * Database table indexing the login attempts of an ip in combination with the timestamp - */ - object LoginAttempts : Table() { - val id = integer("id").autoIncrement().primaryKey() - val ip = varchar("ip", 16) - val timestamp = datetime("timestamp") - } - - /** - * Database table storing general data/states - */ - object General : Table() { - val id = integer("id").autoIncrement().primaryKey() - val initialUse = bool("initialUse").default(true) - val isSetup = bool("isSetup").default(false) - } - - init { - // Create connection - TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE - - // Add tables - transaction { - SchemaUtils.createMissingTablesAndColumns( - FileLocation, - UserData, - UserRoles, - UserRegistration, - RolesData, - LoginAttempts, - General - ) - } - } - - /** - * Creates the user in the database using username, password and the role - */ - fun createUser(usernameString: String, passwordString: String, roleString: String): Boolean { - return transaction { - try { - val usersId = UserData.insert { - it[username] = usernameString - it[password] = BCrypt.withDefaults().hashToString(12, passwordString.toCharArray()) - it[verification] = generateRandomString() - }[UserData.id] - - UserRoles.insert { roles -> - roles[userId] = usersId!! - roles[roleId] = RolesData.select { RolesData.role eq roleString }.map { it[RolesData.id] }[0] - } - true - } catch (_: Exception) { - log.warn("User already exists!") - false - } - } - } - - /** - * Checks whether the user is allowed to register - */ - fun isUserRegistrationValid(usernameString: String, tokenString: String): Boolean { - return transaction { - try { - if (UserData.select { UserData.username eq usernameString }.empty() && - UserRegistration.select { UserRegistration.token eq tokenString }.map { it[UserRegistration.token] }[0] == tokenString - ) { - usernameString == UserRegistration.select { UserRegistration.username eq usernameString }.map { it[UserRegistration.username] }[0] - } else false - } catch (err: Exception) { - log.error(err.toString()) - false - } - } - } - - /** - * Adds a user to the registration table - */ - fun indexUserRegistration(ctx: Context) { - val usernameString = ctx.formParam("username", "").toString() - - if (usernameString.matches("[a-zA-Z0-9]+".toRegex()) && usernameString.length > 3) { - val tokenString = generateRandomString() - var error = false - - transaction { - try { - UserRegistration.insert { - it[username] = usernameString - it[token] = tokenString - } - } catch (err: Exception) { - log.error(err.toString()) - error = true - } - } - - if (error) ctx.render("admin.rocker.html", model("message", "User already exists!", "ctx", ctx)) - else ctx.render( - "admin.rocker.html", model( - "message", "http://${ctx.host()}/user/register?username=$usernameString&token=$tokenString", - "ctx", ctx - ) - ) - } else ctx.render("admin.rocker.html", model("message", "Please only use alphabetical characters!", "ctx", ctx)) - } - - /** - * Removes the registration index of [usernameString] - */ - fun removeRegistrationIndex(usernameString: String) { - transaction { - UserRegistration.deleteWhere { UserRegistration.username eq usernameString } - } - } - - /** - * Tests whether the password [passwordString] of the user [usernameString] is correct - */ - fun checkUser(usernameString: String, passwordString: String): Boolean { - return transaction { - try { - val passwordHash = - UserData.select { UserData.username eq usernameString }.map { it[UserData.password] }[0] - BCrypt.verifyer().verify(passwordString.toCharArray(), passwordHash).verified - } catch (err: Exception) { - log.error(err.toString()) - false - } - } - } - - /** - * Returns the corresponding username using [userId] - */ - fun getUsername(userId: Int): String { - return transaction { - try { - UserData.select { UserData.id eq userId }.map { it[UserData.username] }[0] - } catch (err: Exception) { - log.error(err.toString()) - "" - } - } - } - - /** - * Returns the corresponding username using [verificationId] - */ - fun getUserIdByVerificationId(verificationId: String): Int { - return transaction { - try { - UserData.select { UserData.verification eq verificationId }.map { it[UserData.id] }[0] - } catch (err: Exception) { - -1 - } - } - } - - /** - * Returns true when user uses dark theme - */ - fun isDarkTheme(userId: Int): Boolean { - return transaction { - try { - UserData.select { UserData.id eq userId }.map { it[UserData.darkTheme] }[0] - } catch (err: Exception) { - false - } - } - } - - /** - * Toggles the dark theme - */ - fun toggleDarkTheme(userId: Int) { - return transaction { - try { - UserData.update({ (UserData.id eq userId) }) { - it[darkTheme] = !isDarkTheme(userId) - } - } catch (err: Exception) { - log.error(err.toString()) - } - } - } - - /** - * Returns the corresponding verification id using [usernameString] - */ - fun getVerificationId(usernameString: String): String { - return transaction { - try { - UserData.select { UserData.username eq usernameString }.map { it[UserData.verification] }[0] - } catch (err: Exception) { - log.error(err.toString()) - "" - } - } - } - - /** - * Returns the corresponding userId using [usernameString] - */ - fun getUserId(usernameString: String): Int { - return transaction { - try { - UserData.select { UserData.username eq usernameString }.map { it[UserData.id] }[0] - } catch (err: Exception) { - log.error(err.toString()) - -1 - } - } - } - - /** - * Returns the corresponding role using [userId] - */ - fun getRoles(userId: Int): List<Roles> { - return transaction { - try { - val userRoleId = UserRoles.select { UserRoles.userId eq userId }.map { it[UserRoles.roleId] }[0] - - val userRoles = mutableListOf<Roles>() - RolesData.select { RolesData.id eq userRoleId }.map { it[RolesData.role] }.forEach { - when (Roles.valueOf(it)) { - Roles.GUEST -> { - userRoles.add(Roles.GUEST) - } - Roles.USER -> { - userRoles.add(Roles.USER) - } - Roles.ADMIN -> { - userRoles.add(Roles.GUEST) - userRoles.add(Roles.USER) - userRoles.add(Roles.ADMIN) - } - } - } - userRoles - } catch (err: Exception) { - listOf(Roles.GUEST) - } - } - } - - /** - * Adds the uploaded file to the database - */ - fun addFile(fileLocation: String, usersId: Int, isDirectoryBool: Boolean = false): Boolean { - return transaction { - try { - if (FileLocation.select { (FileLocation.path eq fileLocation) and (FileLocation.userId eq usersId) }.empty()) { - FileLocation.insert { - it[path] = fileLocation - it[userId] = usersId - it[accessId] = generateRandomString() - it[isDirectory] = isDirectoryBool - } - true - } else { - if (!isDirectoryBool && debug) log.warn("File already exists!") - false - } - } catch (err: Exception) { - error(err) - } - } - } - - /** - * Removes the file from the database - */ - fun deleteFile(fileLocation: String, userId: Int) { - transaction { - try { - // TODO: Think of new solution for directory deleting (instead of wildcards) - FileLocation.deleteWhere { (FileLocation.path like "$fileLocation%") and (FileLocation.userId eq userId) } - } catch (err: Exception) { - log.error(err.toString()) - log.warn("File does not exist!") - } - } - } - - /** - * Returns the accessId of the given file - */ - fun getAccessId(fileLocation: String, userId: Int): String { - return transaction { - try { - FileLocation.update({ (FileLocation.userId eq userId) and (FileLocation.path like "$fileLocation%") }) { - it[isShared] = true - } - FileLocation.select { (FileLocation.path eq fileLocation) and (FileLocation.userId eq userId) }.map { it[FileLocation.accessId] }[0] - } catch (err: Exception) { - log.error(err.toString()) - "" - } - } - } - - /** - * Returns accessId of file in directory - */ - fun getAccessIdOfDirectory(filename: String, accessId: String): String { - return transaction { - try { - val fileData = - FileLocation.select { - FileLocation.accessId eq accessId - }.map { it[FileLocation.path] to it[FileLocation.userId] to it[FileLocation.isShared] }[0] - if (fileData.second) - FileLocation.select { - (FileLocation.path eq "${fileData.first.first}${filename.substring(1)}") and (FileLocation.userId eq fileData.first.second) - }.map { it[FileLocation.accessId] }[0] - else "" - } catch (err: Exception) { - log.error(err.toString()) - "" - } - } - } - - /** - * Gets the shared file via [accessId] - */ - fun getSharedFile(accessId: String): ReturnFileData { - return transaction { - try { - if (FileLocation.select { FileLocation.accessId eq accessId }.map { it[FileLocation.isShared] }[0]) { - val userId = - FileLocation.select { FileLocation.accessId eq accessId }.map { it[FileLocation.userId] }[0] - val fileLocation = - FileLocation.select { FileLocation.accessId eq accessId }.map { it[FileLocation.path] }[0] - val isDir = - FileLocation.select { FileLocation.accessId eq accessId }.map { it[FileLocation.isDirectory] }[0] - ReturnFileData(userId, fileLocation, isDir) - } else - ReturnFileData(-1, "", false) - } catch (err: Exception) { - log.error(err.toString()) - log.warn("File does not exist!") - ReturnFileData(-1, "", false) - } - } - } - - /** - * Checks whether the site has been set up - */ - fun isSetup(): Boolean { - return transaction { - try { - General.selectAll().map { it[General.isSetup] }[0] - } catch (err: Exception) { - log.error(err.toString()) - false - } - } - } - - /** - * Toggles the setup state - */ - fun toggleSetup() { - transaction { - General.update({ General.initialUse eq false }) { - it[isSetup] = true - } - } - } - - /** - * Adds an login attempt to the database - */ - fun loginAttempt(dateTime: DateTime, requestIp: String) { - transaction { - LoginAttempts.insert { - it[timestamp] = dateTime - it[ip] = requestIp - } - } - } - - /** - * Gets all login attempts of [requestIp] - */ - fun getLoginAttempts(requestIp: String): List<Pair<DateTime, String>> { - return transaction { - LoginAttempts.select { LoginAttempts.ip eq requestIp } - .map { it[LoginAttempts.timestamp] to it[LoginAttempts.ip] } - } - } - - /** - * Initializes the database - */ - fun initDatabase() { - val initialUseRow = transaction { General.selectAll().map { it[General.initialUse] } } - if (initialUseRow.isEmpty() || initialUseRow[0]) { - transaction { - RolesData.insert { - it[role] = "ADMIN" - } - RolesData.insert { - it[role] = "USER" - } - RolesData.insert { - it[role] = "GUEST" - } - - UserRoles.insert { - it[userId] = 1 - it[roleId] = 1 - } - - General.insert { - it[initialUse] = false - } - } - } else { - log.warn("Already initialized Database.") - } - } - - /** - * Generates a random string with [length] characters - */ - private fun generateRandomString(length: Int = 64): String { - val allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789" - return (1..length) - .map { allowedChars.random() } - .joinToString("") - } -} - -data class ReturnFileData( - val userId: Int, - val fileLocation: String, - val isDirectory: Boolean -) diff --git a/src/main/kotlin/FileController.kt b/src/main/kotlin/FileController.kt deleted file mode 100644 index b1c26dd..0000000 --- a/src/main/kotlin/FileController.kt +++ /dev/null @@ -1,277 +0,0 @@ -package space.anity - -import io.javalin.* -import io.javalin.rendering.template.TemplateUtil.model -import org.slf4j.* -import java.io.* -import java.nio.charset.* -import java.nio.file.* -import java.text.* -import javax.crypto.* -import javax.crypto.spec.* - -class FileController { - private val log = LoggerFactory.getLogger(this.javaClass.name) - - private val staticEncPwd = "asdffdsaasdffdsa" - private val secretKey: SecretKey = SecretKeySpec(staticEncPwd.toByteArray(), "AES") - - /** - * Crawls the requested file and either renders the directory view or the file view - */ - fun crawl(ctx: Context) { - try { - val userId = userHandler.getVerifiedUserId(ctx) - val usersFileHome = "$fileHome/$userId" - val firstParam = ctx.splat(0) ?: "" - val fileLocation = "$usersFileHome/$firstParam" - File(fileLocation).mkdirs() - when { - ctx.queryParam("raw") != null -> ctx.result(decrypt(fileLocation)) - File(fileLocation).isDirectory -> { - val files = ArrayList<Array<String>>() - Files.list(Paths.get("$usersFileHome/$firstParam/")).forEach { - val filename = it.toString() - .drop(usersFileHome.length + (if (firstParam.isNotEmpty()) firstParam.length + 2 else 1)) - val filePath = "$usersFileHome${it.toString().drop(usersFileHome.length)}" - files.add(addToFileListing(filePath, filename)) - } - files.sortWith(compareBy { it.first() }) - ctx.render( - "files.rocker.html", model( - "files", files, - "path", (if (firstParam.firstOrNull() == '/') firstParam.drop(1) else firstParam), - "isShared", false, - "ctx", ctx - ) - ) - } - // TODO: Fix decrypting every file when crawling (human readable flag in db?) - isHumanReadable(decrypt(fileLocation).toByteArray()) -> handleHumanReadableFile(fileLocation, ctx) - else -> { - ctx.contentType(Files.probeContentType(Paths.get(fileLocation))) - ctx.result(decrypt(fileLocation)) - } - } - } catch (err: Exception) { - log.error(err.toString()) - throw NotFoundResponse("Error: File or directory does not exist.") - } - } - - /** - * Decrypts a file using the [fileLocation] and the crypto helping class - */ - private fun decrypt(fileLocation: String): String { - val cryptoHandler = CryptoHandler(secretKey, "AES/CBC/PKCS5Padding") - return cryptoHandler.decrypt(fileLocation) - } - - /** - * Gets directory size recursively - */ - private fun getDirectorySize(directory: File): Long { - var length: Long = 0 - for (file in directory.listFiles()!!) { - length += if (file.isFile) file.length() - else getDirectorySize(file) - } - return length - } - - /** - * Checks whether the file is binary or human-readable (text) - */ - private fun isHumanReadable(data: ByteArray): Boolean { - val text = String(data, Charset.forName("ISO-8859-1")) - val replacedText = text.replace( - ("[a-zA-Z0-9ßöäü\\.\\*!\"§\\$\\%&/()=\\?@~'#:,;\\+><\\|\\[\\]\\{\\}\\^°²³\\\\ \\n\\r\\t_\\-`´âêîôÂÊÔÎáéíóàèìòÁÉÍÓÀÈÌÒ©‰¢£¥€±¿»«¼½¾™ª]").toRegex(), - "" - ) - val d = (text.length - replacedText.length).toDouble() / text.length.toDouble() - return d > 0.95 - } - - /** - * Converts bytes to human-readable text like 100MiB - */ - private fun humanReadableBytes(bytes: Long): String { - val unit = 1024 - if (bytes < unit) return "$bytes B" - val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() - val pre = "KMGTPE"[exp - 1] + "i" - return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre) - } - - /** - * Saves multipart media data into requested directory - */ - fun upload(ctx: Context) { - try { - ctx.uploadedFiles("file").forEach { (_, content, name, _) -> - val fixedName = name.replace(":", "/") // "fix" for Firefox.. - val userId = userHandler.getVerifiedUserId(ctx) - val fileLocation = "$fileHome/$userId/$fixedName" - var addPath = "" - - val stringContent = content.bufferedReader(Charsets.UTF_8).use { it.readText() } - - fixedName.split("/").forEach { - addPath += "$it/" - if (!fixedName.endsWith(it)) databaseController.addFile(addPath, userId, true) - } - - val cryptoHandler = CryptoHandler(secretKey, "AES/CBC/PKCS5Padding") - cryptoHandler.encrypt(stringContent, fileLocation) - databaseController.addFile(fixedName, userId, false) - } - - ctx.json("success") - } catch (err: Exception) { - log.error(err.toString()) - } - } - - /** - * Re-indexes every file in the users directory - */ - fun indexAll(ctx: Context) { - val userId = userHandler.getVerifiedUserId(ctx) - - fun recursiveIndex(filePath: String = "") { - Files.list(Paths.get("$fileHome/$userId$filePath")).forEach { - val filename = it.toString().drop("$fileHome/$userId".length + 1) - - if (it.toFile().isDirectory) { - databaseController.addFile("$filename/", userId, true) - recursiveIndex("/$filename") - } else databaseController.addFile(filename, userId, false) - } - } - - recursiveIndex() - } - - /** - * Deletes the requested file - */ - fun delete(ctx: Context) { - val userId = userHandler.getVerifiedUserId(ctx) - if (userId > 0) { - val path = ctx.splat(0) ?: "" - val file = File("$fileHome/$userId/$path") - - fun deleteDirectory(recursiveFile: File) { - val fileList = recursiveFile.listFiles() - if (fileList != null) { - for (subFile in fileList) { - deleteDirectory(subFile) - } - } - recursiveFile.delete() - } - - if (file.isDirectory) { - deleteDirectory(file) - } else file.delete() - databaseController.deleteFile(path, userId) - } - } - - /** - * Shares the requested file via the accessId - */ - fun share(ctx: Context) { - val userId = userHandler.getVerifiedUserId(ctx) - val shareType = ctx.queryParam("type").toString() - val firstParam = ctx.splat(0) ?: "" - if (userId > 0) { - val path = "$firstParam${if (shareType == "dir") "/" else ""}" - val accessId = databaseController.getAccessId(path, userId) - ctx.result("${ctx.host()}/file/shared?id=$accessId") - } - } - - /** - * Renders a shared file - */ - fun renderShared(ctx: Context) { - val sharedFileData = databaseController.getSharedFile(ctx.queryParam("id").toString()) - val fileLocation = sharedFileData.fileLocation - if (sharedFileData.userId > 0 && fileLocation.isNotEmpty()) { - val sharedFileLocation = "$fileHome/${sharedFileData.userId}/$fileLocation" - if (!sharedFileData.isDirectory) { - if (isHumanReadable(decrypt(sharedFileLocation).toByteArray())) - handleHumanReadableFile(sharedFileLocation, ctx) - else { - // TODO: Fix name of downloaded file ("shared") - ctx.contentType(Files.probeContentType(Paths.get(sharedFileLocation))) - ctx.result(FileInputStream(File(sharedFileLocation))) - } - } else { - val files = ArrayList<Array<String>>() - Files.list(Paths.get(sharedFileLocation)).forEach { - val filename = it.toString() - .drop(sharedFileLocation.length) - val filePath = "$sharedFileLocation$filename" - files.add(addToFileListing(filePath, filename)) - } - files.sortWith(compareBy { it.first() }) - ctx.render( - "files.rocker.html", model( - "files", files, - "path", (if (fileLocation.firstOrNull() == '/') fileLocation.drop(1) else fileLocation), - "isShared", true, - "ctx", ctx - ) - ) - } - } else { - log.warn("Unknown file!") - throw NotFoundResponse("Shared file couldn't be found.") - } - } - - /** - * Adds a file to the file array used in the file listing view - */ - private fun addToFileListing(fileLocation: String, filename: String): Array<String> { - val file = File(fileLocation) - val fileSize = if (file.isDirectory) getDirectorySize(file) else file.length() - return arrayOf( - // TODO: Clean up array responses - if (file.isDirectory) "$filename/" else filename, - humanReadableBytes(fileSize), - SimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(file.lastModified()).toString(), - if (file.isDirectory) "true" - else isHumanReadable(decrypt(fileLocation).toByteArray()).toString(), - file.isDirectory.toString(), - fileSize.toString(), // unformatted file size - file.lastModified().toString() // unformatted last modified date - ) - } - - /** - * Handles the rendering of human readable files - */ - private fun handleHumanReadableFile(fileLocation: String, ctx: Context) { - val content = decrypt(fileLocation) - ctx.render( - "fileview.rocker.html", model( - "content", content, - "filename", File(fileLocation).name, - "extension", File(fileLocation).extension, - "ctx", ctx - ) - ) - } - - /** - * Returns the access id of the directory - */ - fun handleSharedFile(ctx: Context) { - val filename = ctx.formParam("filename").toString() - val accessId = ctx.formParam("accessId").toString() - ctx.result(databaseController.getAccessIdOfDirectory(filename, accessId)) - } -} diff --git a/src/main/kotlin/LogFilter.kt b/src/main/kotlin/LogFilter.kt deleted file mode 100644 index df52f8f..0000000 --- a/src/main/kotlin/LogFilter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package space.anity - -import ch.qos.logback.classic.* -import ch.qos.logback.classic.spi.* -import ch.qos.logback.core.filter.* -import ch.qos.logback.core.spi.* - -class LogFilter : Filter<ILoggingEvent>() { - override fun decide(event: ILoggingEvent): FilterReply { - val isUseful = event.loggerName !in - listOf( - "Exposed", - "io.javalin.Javalin", - "com.fizzed.rocker.runtime.RockerRuntime", - "org.eclipse.jetty.util.log" - ) - - return if (event.level == Level.INFO && isUseful) { - FilterReply.ACCEPT - } else { - if ((!silent || event.message.contains("Help")) && event.level != Level.DEBUG && isUseful) { - FilterReply.ACCEPT - } else { - FilterReply.DENY - } - } - } -} diff --git a/src/main/kotlin/UserHandler.kt b/src/main/kotlin/UserHandler.kt deleted file mode 100644 index 1b0c30a..0000000 --- a/src/main/kotlin/UserHandler.kt +++ /dev/null @@ -1,222 +0,0 @@ -package space.anity - -import io.javalin.* -import io.javalin.rendering.template.TemplateUtil.model -import org.joda.time.* -import org.slf4j.* -import kotlin.math.* - -class UserHandler { - private val log = LoggerFactory.getLogger(this.javaClass.name) - - /** - * Renders the login page - */ - fun renderLogin(ctx: Context) { - if (userHandler.getVerifiedUserId(ctx) > 0 || !databaseController.isSetup()) ctx.redirect("/") - else ctx.render("login.rocker.html", model("message", "", "counter", 0, "ctx", ctx)) - } - - /** - * Checks and verifies users credentials and logs the user in - */ - fun login(ctx: Context) { - if (getVerifiedUserId(ctx) > 0 || !databaseController.isSetup()) ctx.redirect("/") - - val username = ctx.formParam("username").toString() - val password = ctx.formParam("password").toString() - val requestIp = ctx.ip() - - val loginAttempts = databaseController.getLoginAttempts(requestIp) - val lastAttemptDifference = - if (loginAttempts.isEmpty()) -1 - else Interval( - loginAttempts[loginAttempts.indexOfLast { true }].first.toInstant(), - Instant() - ).toDuration().standardSeconds.toInt() - - var lastHourAttempts = 0 - loginAttempts.forEach { - val difference = Interval(it.first.toInstant(), Instant()).toDuration().standardMinutes.toInt() - if (difference < 60) lastHourAttempts += 1 - } - val nextThreshold = 4f.pow(lastHourAttempts + 1) - - if (lastAttemptDifference > 4f.pow(lastHourAttempts) || lastHourAttempts == 0) { - if (databaseController.checkUser(username, password)) { - ctx.cookieStore("verification", databaseController.getVerificationId(username)) - ctx.cookieStore("userId", databaseController.getUserId(username)) - ctx.redirect("/") - } else { - databaseController.loginAttempt(DateTime(), requestIp) - ctx.render( - "login.rocker.html", - model( - "message", - "Login failed!", - "counter", if (nextThreshold / 60 > 60) 3600 else nextThreshold.toInt(), - "ctx", ctx - ) - ) - } - } else { - databaseController.loginAttempt(DateTime(), requestIp) - ctx.render( - "login.rocker.html", - model( - "message", - "Too many request.", - "counter", if (nextThreshold / 60 > 60) 3600 else nextThreshold.toInt(), - "ctx", ctx - ) - ) - } - } - - /** - * Logs the user out of the system - */ - fun logout(ctx: Context) { - ctx.clearCookieStore() - ctx.removeCookie("javalin-cookie-store", "/") - ctx.redirect("/") - } - - /** - * Toggles the users dark theme - */ - fun toggleTheme(ctx: Context) { - databaseController.toggleDarkTheme(userHandler.getVerifiedUserId(ctx)) - val dark = databaseController.isDarkTheme(userHandler.getVerifiedUserId(ctx)) - ctx.json(mapOf("dark" to dark)) - } - - /** - * Renders the admin interface - */ - fun renderAdmin(ctx: Context) { - ctx.render("admin.rocker.html", model("message", "", "ctx", ctx)) - } - - /** - * Renders the setup page - */ - fun renderSetup(ctx: Context) { - if (databaseController.isSetup()) ctx.redirect("/user/login") - else ctx.render("setup.rocker.html", model("message", "", "ctx", ctx)) - } - - /** - * Sets up the general settings and admin credentials - */ - fun setup(ctx: Context) { - try { - val username = ctx.formParam("username").toString() - val password = ctx.formParam("password").toString() - val verifyPassword = ctx.formParam("verifyPassword").toString() - - // TODO: Clean up ugly if statements in validation - if (username.matches("[a-zA-Z0-9]+".toRegex()) && username.length > 3) { - if (password == verifyPassword) { - if (password.length >= 8) - if (databaseController.createUser(username, password, "ADMIN")) { - databaseController.toggleSetup() - ctx.redirect("/user/login") - } else ctx.status(400).render( - "setup.rocker.html", - model("message", "User already exists!", "ctx", ctx) - ) - else ctx.status(400).render( - "setup.rocker.html", - model("message", "Password is too short!", "ctx", ctx) - ) - } else ctx.status(400).render( - "setup.rocker.html", - model("message", "Passwords do not match!", "ctx", ctx) - ) - } else ctx.status(400).render( - "setup.rocker.html", - model("message", "Username must only use alphabetical characters!", "ctx", ctx) - ) - } catch (err: Exception) { - ctx.status(400).render("setup.rocker.html", model("message", "An error occurred!", "ctx", ctx)) - error(err) - } - } - - /** - * Renders the registration page - */ - fun renderRegistration(ctx: Context) { - val username = ctx.queryParam("username", "") - val token = ctx.queryParam("token", "") - - if (username.isNullOrEmpty()) throw ForbiddenResponse("Please provide a valid username!") - else if (token.isNullOrEmpty()) throw ForbiddenResponse("Please provide a valid token!") - else { - if (databaseController.isUserRegistrationValid(username, token)) - ctx.render( - "register.rocker.html", - model("username", username, "token", token, "message", "", "ctx", ctx) - ) - else ctx.redirect("/user/login") - } - } - - /** - * Registers a new user - */ - fun register(ctx: Context) { - try { - val username = ctx.formParam("username").toString() - val token = ctx.formParam("token").toString() - val password = ctx.formParam("password").toString() - val verifyPassword = ctx.formParam("verifyPassword").toString() - - if (password == verifyPassword) { - if (password.length >= 8) - if (databaseController.isUserRegistrationValid(username, token)) { - databaseController.createUser(username, password, "USER") - databaseController.removeRegistrationIndex(username) - ctx.redirect("/user/login") - } else ctx.render( - "register.rocker.html", - model("username", username, "token", token, "message", "Not authorized!", "ctx", ctx) - ) - else ctx.render( - "register.rocker.html", - model( - "username", username, - "token", token, - "message", "Please make sure that your password is at least 8 digits long!", - "ctx", ctx - ) - ) - } else ctx.render( - "register.rocker.html", - model("username", username, "token", token, "message", "The passwords don't match!", "ctx", ctx) - ) - } catch (err: Exception) { - throw BadRequestResponse() - } - } - - /** - * Gets the username and verifies its identity - */ - fun getVerifiedUserId(ctx: Context): Int { - return if (databaseController.getUserIdByVerificationId(ctx.cookieStore("verification") ?: "verification") - == ctx.cookieStore("userId") ?: "userId" - ) ctx.cookieStore("userId") - else -1 - } - - /** - * Checks whether a user has admin privileges - */ - fun isAdmin(usernameString: String): Boolean { - val userId = databaseController.getUserId(usernameString) - return if (userId > 0) databaseController.getRoles(userId).contains(Roles.ADMIN) - else false - } -} |