diff --git a/build.gradle b/build.gradle index c4af8a64492ab7e43c17cf37458b714df3e4e6cb..85ad63cd5782cd2b608a5946fbd5bb1487255ddb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.5.30' ext.hilt_version = '2.38.1' ext.tor_version = '0.3.5.15' ext.obfs4_version = '0.0.12-dev-40245c4a' @@ -11,7 +11,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + classpath 'com.android.tools.build:gradle:7.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" } diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle index 33f0927ea6db736a0c4093673186d6525bff656e..cbf2b5ce91682019cceb3b02207fc2646bfa496b 100644 --- a/mailbox-core/build.gradle +++ b/mailbox-core/build.gradle @@ -19,9 +19,10 @@ dependencies { implementation 'org.briarproject:jtorctl:0.3' - def ktorVersion = '1.6.2' - implementation "io.ktor:ktor-server-core:$ktorVersion" - implementation "io.ktor:ktor-server-netty:$ktorVersion" + def ktor_version = '1.6.2' + implementation "io.ktor:ktor-server-core:$ktor_version" + implementation "io.ktor:ktor-server-netty:$ktor_version" + implementation "io.ktor:ktor-auth:$ktor_version" api "org.slf4j:slf4j-api:1.7.32" implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6 @@ -32,7 +33,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit_version" testImplementation "io.mockk:mockk:$mockk_version" testImplementation "ch.qos.logback:logback-classic:1.2.5" - testImplementation "io.ktor:ktor-client-cio:$ktorVersion" + testImplementation "io.ktor:ktor-client-cio:$ktor_version" testImplementation "com.google.dagger:hilt-core:$hilt_version" kaptTest "com.google.dagger:dagger-compiler:$hilt_version" } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt index 56e1dd9fa81c6d384e4b8a7a9a5440dfeca440c7..e108f9da656e355e7ef445cca88cde27ed214cc1 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt @@ -29,7 +29,13 @@ interface Database : TransactionManager { @Throws(DbException::class) fun getContact(txn: Transaction, id: Int): Contact? + @Throws(DbException::class) + fun getContacts(txn: Transaction): List<Contact> + @Throws(DbException::class) fun removeContact(txn: Transaction, id: Int) + @Throws(DbException::class) + fun getContactWithToken(txn: Transaction, token: String): Contact? + } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt index b907191fc10b505e5048dab19c4b78f46f2627a0..651263331b3c9c85bf447f1d22a93e188eaeab7c 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt @@ -147,8 +147,8 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc return true } - // Public access for testing - fun getMigrations(): List<Migration<Connection>> { + @Suppress("MemberVisibilityCanBePrivate") // visible for testing + internal fun getMigrations(): List<Migration<Connection>> { return Arrays.asList<Migration<Connection>>( // Migration1_2(dbTypes), ) @@ -488,6 +488,33 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } } + @Throws(DbException::class) + override fun getContacts(txn: Transaction): List<Contact> { + val contacts = ArrayList<Contact>() + val connection: Connection = txn.unbox() + var ps: PreparedStatement? = null + var rs: ResultSet? = null + try { + val sql = "SELECT contactId, token, inbox, outbox FROM contacts" + ps = connection.prepareStatement(sql) + rs = ps.executeQuery() + while (rs.next()) { + val id = rs.getInt(1) + val token = rs.getString(2) + val inboxId = rs.getString(3) + val outboxId = rs.getString(4) + contacts.add(Contact(id, token, inboxId, outboxId)) + } + rs.close() + ps.close() + return contacts + } catch (e: SQLException) { + tryToClose(rs, LOG) + tryToClose(ps, LOG) + throw DbException(e) + } + } + @Throws(DbException::class) override fun removeContact(txn: Transaction, id: Int) { val connection: Connection = txn.unbox() @@ -505,6 +532,31 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } } + override fun getContactWithToken(txn: Transaction, token: String): Contact? { + val connection: Connection = txn.unbox() + var ps: PreparedStatement? = null + var rs: ResultSet? = null + try { + val sql = """SELECT contactId, inbox, outbox FROM contacts + WHERE token = ? + """.trimIndent() + ps = connection.prepareStatement(sql) + ps.setString(1, token) + rs = ps.executeQuery() + if (!rs.next()) return null + val id = rs.getInt(1) + val inboxId = rs.getString(2) + val outboxId = rs.getString(3) + rs.close() + ps.close() + return Contact(id, token, inboxId, outboxId) + } catch (e: SQLException) { + tryToClose(rs, LOG) + tryToClose(ps, LOG) + throw DbException(e) + } + } + /** * Commits a transaction to the database. */ diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..1c8453b8516351a225252ef6f31941bc5acb41ad --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt @@ -0,0 +1,112 @@ +package org.briarproject.mailbox.core.files + +import io.ktor.application.ApplicationCall +import io.ktor.auth.principal +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.server.AuthException +import org.briarproject.mailbox.core.server.AuthManager +import org.briarproject.mailbox.core.server.MailboxPrincipal +import org.briarproject.mailbox.core.system.InvalidIdException +import org.briarproject.mailbox.core.system.RandomIdManager +import javax.inject.Inject + +class FileManager @Inject constructor( + private val db: Database, + private val authManager: AuthManager, + private val randomIdManager: RandomIdManager, +) { + + /** + * Used by contacts to send files to the owner and by the owner to send files to contacts. + * + * Checks if the authenticated [MailboxPrincipal] is allowed to upload to given [folderId], + * Responds with 200 (OK) if upload was successful + * (no 201 as the uploader doesn't need to know the $fileId) + * The mailbox chooses a random ID string for the file ID. + */ + @Throws(AuthException::class, InvalidIdException::class) + suspend fun postFile(call: ApplicationCall, folderId: String) { + val principal: MailboxPrincipal? = call.principal() + randomIdManager.assertIsRandomId(folderId) + authManager.assertCanPostToFolder(principal, folderId) + + // TODO implement + + call.respond(HttpStatusCode.OK, "post: Not yet implemented. folderId: $folderId}") + } + + /** + * Used by owner and contacts to list their files to retrieve. + * + * Checks if the authenticated [MailboxPrincipal] is allowed to download from [folderId]. + * Responds with 200 (OK) with the list of files in JSON. + */ + suspend fun listFiles(call: ApplicationCall, folderId: String) { + val principal: MailboxPrincipal? = call.principal() + randomIdManager.assertIsRandomId(folderId) + authManager.assertCanDownloadFromFolder(principal, folderId) + + // TODO implement + + call.respond(HttpStatusCode.OK, "get: Not yet implemented. folderId: $folderId") + } + + /** + * Used by owner and contacts to retrieve a file. + * + * Checks if the authenticated [MailboxPrincipal] is allowed to download from $folderId + * Returns 200 (OK) if successful with the files' bytes in the response body + */ + @Throws(AuthException::class, InvalidIdException::class) + suspend fun getFile(call: ApplicationCall, folderId: String, fileId: String) { + val principal: MailboxPrincipal? = call.principal() + randomIdManager.assertIsRandomId(folderId) + randomIdManager.assertIsRandomId(fileId) + authManager.assertCanDownloadFromFolder(principal, folderId) + + // TODO implement + + call.respond( + HttpStatusCode.OK, + "get: Not yet implemented. folderId: $folderId fileId: $fileId" + ) + } + + /** + * Used by owner and contacts to delete files. + * + * Checks if the authenticated [MailboxPrincipal] is allowed to download from [folderId]. + * Responds with 200 (OK) if deletion was successful. + */ + suspend fun deleteFile(call: ApplicationCall, folderId: String, fileId: String) { + val principal: MailboxPrincipal? = call.principal() + randomIdManager.assertIsRandomId(folderId) + randomIdManager.assertIsRandomId(fileId) + authManager.assertCanDownloadFromFolder(principal, folderId) + + // TODO implement + + call.respond( + HttpStatusCode.OK, + "delete: Not yet implemented. folderId: $folderId fileId: $fileId" + ) + } + + /** + * Used by owner only to list all folders that have files available for download. + * + * Checks if provided auth token is the owner. + * Responds with 200 (OK) with the list of folders with files in JSON. + */ + suspend fun listFoldersWithFiles(call: ApplicationCall) { + val principal: MailboxPrincipal? = call.principal() + authManager.assertIsOwner(principal) + + // TODO implement + + call.respond(HttpStatusCode.OK, "get: Not yet implemented") + } + +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d4f5a8a1f533c359862249344b757dfe51018c9 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt @@ -0,0 +1,91 @@ +package org.briarproject.mailbox.core.server + +import io.ktor.auth.Principal +import org.briarproject.mailbox.core.api.Contact +import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.server.MailboxPrincipal.ContactPrincipal +import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal +import org.briarproject.mailbox.core.settings.SettingsManager +import org.briarproject.mailbox.core.system.RandomIdManager +import javax.inject.Inject +import javax.inject.Singleton + +// We might want to move this somewhere else later +internal const val SETTINGS_NAMESPACE_OWNER = "owner" +internal const val SETTINGS_OWNER_TOKEN = "ownerToken" + +@Singleton +class AuthManager @Inject constructor( + private val db: Database, + private val settingsManager: SettingsManager, + private val randomIdManager: RandomIdManager, +) { + + /** + * Returns the principal the given token belongs to + * or null if this token doesn't belong to any principal. + */ + fun getPrincipal(token: String): MailboxPrincipal? { + randomIdManager.assertIsRandomId(token) + return db.transactionWithResult(true) { txn -> + val contact = db.getContactWithToken(txn, token) + if (contact != null) { + ContactPrincipal(contact) + } else { + val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) + if (token == settings[SETTINGS_OWNER_TOKEN]) OwnerPrincipal + else null + } + } + } + + /** + * @throws [AuthException] when given [principal] is NOT allowed + * to download or delete from the given [folderId] which is assumed to be validated already. + */ + @Throws(AuthException::class) + fun assertCanDownloadFromFolder(principal: MailboxPrincipal?, folderId: String) { + if (principal == null) throw AuthException() + + if (principal is OwnerPrincipal) { + val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) } + val noOutboxFound = contacts.none { c -> folderId == c.outboxId } + if (noOutboxFound) throw AuthException() + } else if (principal is ContactPrincipal) { + if (folderId != principal.contact.inboxId) throw AuthException() + } + } + + /** + * @throws [AuthException] when given [principal] is NOT allowed + * to post to the given [folderId] which is assumed to be validated already. + */ + @Throws(AuthException::class) + fun assertCanPostToFolder(principal: MailboxPrincipal?, folderId: String) { + if (principal == null) throw AuthException() + + if (principal is OwnerPrincipal) { + val contacts = db.transactionWithResult(true) { txn -> db.getContacts(txn) } + val noInboxFound = contacts.none { c -> folderId == c.inboxId } + if (noInboxFound) throw AuthException() + } else if (principal is ContactPrincipal) { + if (folderId != principal.contact.outboxId) throw AuthException() + } + } + + /** + * @throws [AuthException] when given [principal] is NOT the mailbox owner. + */ + @Throws(AuthException::class) + fun assertIsOwner(principal: MailboxPrincipal?) { + if (principal !is OwnerPrincipal) throw AuthException() + } + +} + +sealed class MailboxPrincipal : Principal { + object OwnerPrincipal : MailboxPrincipal() + class ContactPrincipal(val contact: Contact) : MailboxPrincipal() +} + +class AuthException : IllegalStateException() diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..808f764838e74bb6b399b6830aa638a50497cf30 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/BearerAuthenticationProvider.kt @@ -0,0 +1,122 @@ +package org.briarproject.mailbox.core.server + +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.auth.Authentication +import io.ktor.auth.AuthenticationContext +import io.ktor.auth.AuthenticationFailedCause +import io.ktor.auth.AuthenticationFunction +import io.ktor.auth.AuthenticationPipeline +import io.ktor.auth.AuthenticationProvider +import io.ktor.auth.UnauthorizedResponse +import io.ktor.auth.parseAuthorizationHeader +import io.ktor.http.auth.HttpAuthHeader +import io.ktor.request.httpMethod +import io.ktor.response.respond +import io.ktor.util.pipeline.PipelineContext +import org.briarproject.mailbox.core.util.LogUtils.debug +import org.slf4j.LoggerFactory.getLogger + +private val AUTH_KEY_BEARER: Any = "BearerAuth" +private val LOG = getLogger(BearerAuthenticationProvider::class.java) + +internal class BearerAuthenticationProvider constructor(config: Configuration) : + AuthenticationProvider(config) { + + internal val realm: String = "Briar Mailbox" + internal val authHeader: (ApplicationCall) -> HttpAuthHeader? = { call -> + try { + call.request.parseAuthorizationHeader() + } catch (ex: IllegalArgumentException) { + LOG.warn("Illegal HTTP auth header", ex) + null + } + } + internal val authenticationFunction = config.authenticationFunction + + internal class Configuration internal constructor(name: String?) : + AuthenticationProvider.Configuration(name) { + + /** + * This function is applied to every call with a [String] auth token. + * @return a [MailboxPrincipal] or `null` + */ + var authenticationFunction: AuthenticationFunction<String> = { + throw NotImplementedError( + "Bearer auth authenticationFunction is not specified." + + "Use bearer { authenticationFunction = { ... } } to fix." + ) + } + + internal fun build() = BearerAuthenticationProvider(this) + } + +} + +/** + * Installs Bearer token Authentication mechanism + */ +internal fun Authentication.Configuration.bearer( + name: String? = null, + configure: BearerAuthenticationProvider.Configuration.() -> Unit, +) { + val provider = BearerAuthenticationProvider.Configuration(name).apply(configure).build() + provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context -> + authenticate(context, provider, name) + } + register(provider) +} + +private suspend fun PipelineContext<AuthenticationContext, ApplicationCall>.authenticate( + context: AuthenticationContext, + provider: BearerAuthenticationProvider, + name: String?, +) { + val authHeader = provider.authHeader(call) + if (authHeader == null) { + context.unauthorizedResponse(AuthenticationFailedCause.NoCredentials, provider) + return + } + + try { + val token = (authHeader as? HttpAuthHeader.Single)?.blob + if (token == null) { + context.unauthorizedResponse(AuthenticationFailedCause.InvalidCredentials, provider) + return + } + + // TODO remove logging before release + LOG.debug { "name: $name" } + LOG.debug { "httpMethod: ${call.request.httpMethod}" } + + val principal = provider.authenticationFunction(call, token) + if (principal == null) { + context.unauthorizedResponse(AuthenticationFailedCause.InvalidCredentials, provider) + } else { + context.principal(principal) + } + } catch (cause: Throwable) { + val message = cause.message ?: cause.javaClass.simpleName + LOG.debug { "Bearer verification failed: $message" } + context.error(AUTH_KEY_BEARER, AuthenticationFailedCause.Error(message)) + } +} + +private fun AuthenticationContext.unauthorizedResponse( + cause: AuthenticationFailedCause, + provider: BearerAuthenticationProvider, +) { + challenge(AUTH_KEY_BEARER, cause) { + call.respond( + UnauthorizedResponse( + HttpAuthHeader.Parameterized( + authScheme = "Bearer", + parameters = mapOf(HttpAuthHeader.Parameters.Realm to provider.realm) + ) + ) + ) + if (!it.completed && call.response.status() != null) { + it.complete() + } + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt index f4c9829b087188893e7eba301f99a1ef89e3b441..526ff542f8d8d094d5b264ccdcc9659b175efa0c 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt @@ -1,14 +1,112 @@ package org.briarproject.mailbox.core.server import io.ktor.application.Application +import io.ktor.application.ApplicationCall import io.ktor.application.call +import io.ktor.auth.authenticate import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond import io.ktor.response.respondText +import io.ktor.routing.delete import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.put +import io.ktor.routing.route import io.ktor.routing.routing +import io.ktor.util.getOrFail +import org.briarproject.mailbox.core.files.FileManager +import org.briarproject.mailbox.core.system.InvalidIdException -internal fun Application.configureRouting() = routing { - get("/") { - call.respondText("Hello world!", ContentType.Text.Plain) +internal const val V = "/" // TODO set to "/v1" for release + +internal fun Application.configureBasicApi() = routing { + route(V) { + get { + call.respondText("Hello world!", ContentType.Text.Plain) + } + authenticate { + delete { + call.respond(HttpStatusCode.OK, "delete: Not yet implemented") + } + put("/setup") { + call.respond(HttpStatusCode.OK, "put: Not yet implemented") + } + } + } +} + +internal fun Application.configureContactApi() = routing { + authenticate { + route("$V/contacts") { + put("/{contactId}") { + call.respond( + HttpStatusCode.OK, + "get: Not yet implemented. " + + "contactId: ${call.parameters["contactId"]}" + ) + } + delete("/{contactId}") { + call.respond( + HttpStatusCode.OK, + "delete: Not yet implemented. " + + "contactId: ${call.parameters["contactId"]}" + ) + } + get { + call.respond(HttpStatusCode.OK, "get: Not yet implemented") + } + } + } +} + +internal fun Application.configureFilesApi(fileManager: FileManager) = routing { + + authenticate { + route("$V/files/{folderId}") { + post { + call.handle { + fileManager.postFile(call, call.parameters.getOrFail("folderId")) + } + } + get { + call.handle { + fileManager.listFiles(call, call.parameters.getOrFail("folderId")) + } + } + route("/{fileId}") { + get { + val folderId = call.parameters.getOrFail("folderId") + val fileId = call.parameters.getOrFail("fileId") + call.handle { + fileManager.getFile(call, folderId, fileId) + } + } + delete { + val folderId = call.parameters.getOrFail("folderId") + val fileId = call.parameters.getOrFail("fileId") + call.handle { + fileManager.deleteFile(call, folderId, fileId) + } + } + } + } + } + authenticate { + get("$V/folders") { + call.handle { + fileManager.listFoldersWithFiles(call) + } + } + } +} + +private suspend fun ApplicationCall.handle(block: suspend () -> Unit) { + try { + block() + } catch (e: AuthException) { + respond(HttpStatusCode.Unauthorized, HttpStatusCode.Unauthorized.description) + } catch (e: InvalidIdException) { + respond(HttpStatusCode.BadRequest, "Malformed ID: ${e.id}") } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt index 56233b647c0e98a08fbd3d157a8da0fa24880a99..8cecb21ae2b1fa458d5147ed13a6c21dc028bf77 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt @@ -1,26 +1,49 @@ package org.briarproject.mailbox.core.server import io.ktor.application.install +import io.ktor.auth.Authentication import io.ktor.features.CallLogging import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.lifecycle.Service +import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT +import org.slf4j.Logger import org.slf4j.LoggerFactory.getLogger import javax.inject.Inject import javax.inject.Singleton +interface WebServerManager : Service { + companion object { + const val PORT: Int = 8000 + } +} + @Singleton -class WebServerManager @Inject constructor() : Service { +internal class WebServerManagerImpl @Inject constructor( + private val authManager: AuthManager, + private val fileManager: FileManager, +) : WebServerManager { internal companion object { - internal const val PORT = 8000 - private val LOG = getLogger(WebServerManager::class.java) + private val LOG: Logger = getLogger(WebServerManager::class.java) } private val server by lazy { embeddedServer(Netty, PORT, watchPaths = emptyList()) { install(CallLogging) - configureRouting() + install(Authentication) { + bearer { + authenticationFunction = { token -> + // TODO: Remove logging of token before release. + LOG.error("token: $token") + authManager.getPrincipal(token) + } + } + } + configureBasicApi() + configureContactApi() + configureFilesApi(fileManager) } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt index 37550a3823539a0ab420a0b060c9c0f6a9a6b0aa..be2c92aa2bde8e1495fa490b75971aed979c18b6 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerModule.kt @@ -13,8 +13,10 @@ internal class WebServerModule { @Provides @Singleton - fun provideWebServer(lifecycleManager: LifecycleManager): WebServerManager { - val webServerManager = WebServerManager() + fun provideWebServerManager( + lifecycleManager: LifecycleManager, + webServerManager: WebServerManagerImpl, + ): WebServerManager { lifecycleManager.registerService(webServerManager) return webServerManager } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt index 15bcaa4cbe95d1e6b928c22f21892a53f2b46b8f..e7379b9903d5ebece7825fad8523a8770e57aec3 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt @@ -6,6 +6,10 @@ import javax.inject.Inject private const val ID_SIZE = 32 private const val ID_HEX_SIZE = ID_SIZE * 2 +/** + * Generates and validates random IDs + * that are being used for auth tokens, folder IDs and file names. + */ class RandomIdManager @Inject constructor() { private val secureRandom = SecureRandom() @@ -22,8 +26,15 @@ class RandomIdManager @Inject constructor() { return idRegex.matches(id) } + @Throws(InvalidIdException::class) + fun assertIsRandomId(id: String) { + if (!isValidRandomId(id)) throw InvalidIdException(id) + } + } +class InvalidIdException(val id: String) : IllegalStateException() + fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt index 46e7614f9c2a5b0ec1df63790aa06b9e7f7a5f23..fc4368b83a695946a4bc2103025a602178156092 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt @@ -8,27 +8,27 @@ object LogUtils { private const val NANOS_PER_MILLI = 1000 * 1000 @JvmStatic - fun Logger.trace(msg: () -> String) { + inline fun Logger.trace(msg: () -> String) { if (isTraceEnabled) trace(msg()) } @JvmStatic - fun Logger.debug(msg: () -> String) { + inline fun Logger.debug(msg: () -> String) { if (isDebugEnabled) debug(msg()) } @JvmStatic - fun Logger.info(msg: () -> String) { + inline fun Logger.info(msg: () -> String) { if (isInfoEnabled) info(msg()) } @JvmStatic - fun Logger.warn(msg: () -> String) { + inline fun Logger.warn(msg: () -> String) { if (isWarnEnabled) warn(msg()) } @JvmStatic - fun Logger.error(msg: () -> String) { + inline fun Logger.error(msg: () -> String) { if (isErrorEnabled) error(msg()) } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt index 20b9590c42713f4fc9ec144fbc436cdd995ec4db..b269d9dcb7d5e8e5649615da87c6a61dca813134 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt @@ -32,7 +32,7 @@ internal class TestModule(private val tempDir: File) { @Provides fun provideDatabaseConfig() = object : DatabaseConfig { override fun getDatabaseDirectory(): File { - return tempDir + return File(tempDir, "db") } } } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt index 1fb1141181e6392c6765105dfe09e6110581e7df..ac4d1a7909a2ab29b4725e08b26121b10e32de6a 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt @@ -4,6 +4,7 @@ import org.briarproject.mailbox.core.TestUtils.deleteTestDirectory import org.briarproject.mailbox.core.api.Contact import org.briarproject.mailbox.core.settings.Settings import org.briarproject.mailbox.core.system.Clock +import org.briarproject.mailbox.core.system.RandomIdManager import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.io.File @@ -15,6 +16,8 @@ abstract class JdbcDatabaseTest { @TempDir lateinit var testDir: File + private val randomIdManager = RandomIdManager() + protected abstract fun createDatabase( config: DatabaseConfig, clock: Clock, @@ -37,16 +40,16 @@ abstract class JdbcDatabaseTest { open fun testPersistence() { // Store some records val contact1 = Contact( - 1, - "4291ad1d-897d-4db4-9de9-ea3f78c5262e", - "f21467bd-afb0-4c0e-9090-cae45ea1eae9", - "880629fb-3226-41d8-a978-7b28cf44d57d" + id = 1, + token = randomIdManager.getNewRandomId(), + inboxId = randomIdManager.getNewRandomId(), + outboxId = randomIdManager.getNewRandomId() ) val contact2 = Contact( - 2, - "fbbe9a63-2f28-46d4-a465-e6ca57a5d811", - "7931fa7a-077e-403a-8487-63261027d6d2", - "12a61ca3-af0a-41d1-acc1-a0f4625f6e42" + id = 2, + token = randomIdManager.getNewRandomId(), + inboxId = randomIdManager.getNewRandomId(), + outboxId = randomIdManager.getNewRandomId() ) var db: Database = open(false) db.transaction(false) { txn -> @@ -63,6 +66,9 @@ abstract class JdbcDatabaseTest { val contact2Reloaded1 = db.getContact(txn, 2) assertEquals(contact1, contact1Reloaded1) assertEquals(contact2, contact2Reloaded1) + assertEquals(contact1, db.getContactWithToken(txn, contact1.token)) + assertEquals(contact2, db.getContactWithToken(txn, contact2.token)) + assertNull(db.getContactWithToken(txn, randomIdManager.getNewRandomId())) // Delete one of the records db.removeContact(txn, 1) @@ -92,7 +98,7 @@ abstract class JdbcDatabaseTest { merged["foo"] = "bar" merged["baz"] = "qux" - var db: Database = open(false) + val db: Database = open(false) db.transaction(false) { txn -> // store 'before' db.mergeSettings(txn, before, "namespace") diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..546e5ce16d0328a75371b8ad252e02d1dc543c55 --- /dev/null +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt @@ -0,0 +1,5 @@ +package org.briarproject.mailbox.core.server + +class AuthManagerTest { + // TODO write unit tests +}