aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/App.kt241
-rw-r--r--src/main/kotlin/CryptoHandler.kt51
-rw-r--r--src/main/kotlin/DatabaseController.kt511
-rw-r--r--src/main/kotlin/FileController.kt277
-rw-r--r--src/main/kotlin/LogFilter.kt28
-rw-r--r--src/main/kotlin/UserHandler.kt222
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
- }
-}