aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarvin Borner2019-04-13 17:08:53 +0200
committerMarvin Borner2019-04-13 17:08:53 +0200
commita1e2fe500b3d3947d05e16698488f99e49f29847 (patch)
tree0a94ec9291514fbd9054d80ca6f333d83b06f451
parentcf0c64c6445f618cd8cf523d37e455ba669c5d69 (diff)
Added bruteforce detection
-rw-r--r--src/main/kotlin/App.kt47
-rw-r--r--src/main/kotlin/DatabaseController.kt76
-rw-r--r--src/main/resources/views/login.rocker.html6
3 files changed, 107 insertions, 22 deletions
diff --git a/src/main/kotlin/App.kt b/src/main/kotlin/App.kt
index 31fdae7..a958479 100644
--- a/src/main/kotlin/App.kt
+++ b/src/main/kotlin/App.kt
@@ -10,13 +10,16 @@ import io.javalin.rendering.*
import io.javalin.rendering.template.TemplateUtil.model
import io.javalin.security.*
import io.javalin.security.SecurityUtil.roles
+import org.joda.time.*
import java.io.*
import java.nio.charset.*
import java.nio.file.*
import java.util.*
import java.util.logging.*
+import kotlin.math.*
const val fileHome = "files"
+// TODO: user home directory
val databaseController = DatabaseController()
private val log = Logger.getLogger("App.kt")
@@ -53,7 +56,7 @@ fun main() {
/**
* Endpoint for user authentication
*/
- post("/login", { ctx -> login(ctx) }, roles(Roles.GUEST)) // TODO: brute-force protection
+ post("/login", { ctx -> login(ctx) }, roles(Roles.GUEST))
/**
* Renders the setup page (only on initial use)
@@ -159,7 +162,8 @@ fun crawlFiles(ctx: Context) {
fun upload(ctx: Context) {
ctx.uploadedFiles("file").forEach { (contentType, content, name, extension) ->
FileUtil.streamToFile(content, "$fileHome/${ctx.splats()[0]}/$name")
- // databaseController.addFile("$fileHome/${ctx.splats()[0]}/$name", USER???: get by Session)
+ val userId = databaseController.getUserId(ctx.cookieStore("username"))
+ databaseController.addFile("$fileHome/${ctx.splats()[0]}/$name", if (userId > 0) userId else -1)
ctx.redirect("/upload")
}
}
@@ -190,13 +194,40 @@ private fun isHumanReadable(filePath: String): Boolean {
fun login(ctx: Context) {
val username = ctx.formParam("username").toString()
val password = ctx.formParam("password").toString()
+ val requestIp = ctx.ip()
- if (databaseController.checkUser(username, password)) {
- ctx.cookieStore("uuid", databaseController.getUUID(username))
- ctx.cookieStore("username", username)
- ctx.render("login.rocker.html", model("message", "Login succeeded!"))
- } else
- ctx.render("login.rocker.html", model("message", "Login failed!"))
+ 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 threshold = 4f.pow(lastHourAttempts)
+
+ if (lastAttemptDifference > threshold) {
+ if (databaseController.checkUser(username, password)) {
+ ctx.cookieStore("uuid", databaseController.getUUID(username))
+ ctx.cookieStore("username", username)
+ ctx.render("login.rocker.html", model("message", "Login succeeded!"))
+ } else {
+ databaseController.loginAttempt(DateTime(), requestIp)
+ ctx.render("login.rocker.html", model("message", "Login failed!"))
+ }
+ } else {
+ databaseController.loginAttempt(DateTime(), requestIp)
+ ctx.render(
+ "login.rocker.html",
+ model(
+ "message", "Please try again in ${if (threshold / 60 > 60) "3600" else threshold.toString()} seconds."
+ )
+ )
+ }
}
/**
diff --git a/src/main/kotlin/DatabaseController.kt b/src/main/kotlin/DatabaseController.kt
index 8b6e457..4788057 100644
--- a/src/main/kotlin/DatabaseController.kt
+++ b/src/main/kotlin/DatabaseController.kt
@@ -3,6 +3,7 @@ package space.anity
import at.favre.lib.crypto.bcrypt.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.*
+import org.joda.time.*
import java.sql.*
import java.util.*
import java.util.logging.*
@@ -17,7 +18,7 @@ class DatabaseController(dbFileLocation: String = "main.db") {
object FileLocation : Table() {
val id = integer("id").autoIncrement().primaryKey()
val location = text("location").uniqueIndex()
- val username = varchar("username", 24)
+ val userId = integer("userId").references(UserData.id)
}
/**
@@ -32,6 +33,7 @@ class DatabaseController(dbFileLocation: String = "main.db") {
/**
* Database table indexing the users with their regarding role (multi line per user)
+ * TODO: Add support for multiple roles per user (read, write, edit, etc)
*/
object UserRoles : Table() {
val id = integer("id").autoIncrement().primaryKey()
@@ -48,12 +50,21 @@ class DatabaseController(dbFileLocation: String = "main.db") {
}
/**
+ * 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 = integer("initialUse").default(1).primaryKey()
- val isSetup = integer("isSetup").default(0).primaryKey()
+ val initialUse = bool("initialUse").default(true).primaryKey()
+ val isSetup = bool("isSetup").default(false).primaryKey()
}
init {
@@ -62,7 +73,14 @@ class DatabaseController(dbFileLocation: String = "main.db") {
// Add tables
transaction {
- SchemaUtils.createMissingTablesAndColumns(FileLocation, UserData, UserRoles, RolesData, General)
+ SchemaUtils.createMissingTablesAndColumns(
+ FileLocation,
+ UserData,
+ UserRoles,
+ RolesData,
+ LoginAttempts,
+ General
+ )
}
}
@@ -132,6 +150,20 @@ class DatabaseController(dbFileLocation: String = "main.db") {
}
/**
+ * 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 (_: Exception) {
+ log.warning("User not found!")
+ -1
+ }
+ }
+ }
+
+ /**
* Returns the corresponding role using [usernameString]
*/
fun getRole(usernameString: String): Roles {
@@ -150,12 +182,12 @@ class DatabaseController(dbFileLocation: String = "main.db") {
/**
* Adds the uploaded file to the database
*/
- fun addFile(fileLocation: String, usernameString: String) {
+ fun addFile(fileLocation: String, usersId: Int) {
transaction {
try {
FileLocation.insert {
it[location] = fileLocation
- it[username] = usernameString
+ it[userId] = usersId
}
} catch (_: org.jetbrains.exposed.exceptions.ExposedSQLException) {
log.warning("File already exists!")
@@ -169,7 +201,7 @@ class DatabaseController(dbFileLocation: String = "main.db") {
fun isSetup(): Boolean {
return transaction {
try {
- General.selectAll().map { it[General.isSetup] }[0] == 1
+ General.selectAll().map { it[General.isSetup] }[0]
} catch (_: Exception) {
false
}
@@ -181,18 +213,40 @@ class DatabaseController(dbFileLocation: String = "main.db") {
*/
fun toggleSetup() {
transaction {
- General.update({ General.initialUse eq 0 }) {
- it[General.isSetup] = 1
+ General.update({ General.initialUse eq false }) {
+ it[General.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] == 1) {
+ if (initialUseRow.isEmpty() || initialUseRow[0]) {
transaction {
RolesData.insert {
it[role] = "ADMIN"
@@ -210,7 +264,7 @@ class DatabaseController(dbFileLocation: String = "main.db") {
}
General.insert {
- it[initialUse] = 0
+ it[initialUse] = false
}
}
} else {
diff --git a/src/main/resources/views/login.rocker.html b/src/main/resources/views/login.rocker.html
index 389d82b..66d9ba2 100644
--- a/src/main/resources/views/login.rocker.html
+++ b/src/main/resources/views/login.rocker.html
@@ -1,12 +1,12 @@
@args (String message)
+
@layout.template("Login", RockerContent.NONE, RockerContent.NONE) -> {
-<form action="login" method="post">
+<form action="login" id="login-form" method="post">
<div>
<label for="username">Username:</label>
<input id="username" name="username" type="text"/>
- </div>
- <div>
+
<label for="password">Password:</label>
<input id="password" name="password" type="password"/>
</div>