diff --git a/README.md b/README.md index 4b1317f06d677de4da99e0e42f35c8788aecb0a1..34f5615eaff536c2c03505da2d32ab50fe32656a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,11 @@ any hardware supporting Java (e.g. unix server, raspberry pi) could be added. [briarproject.org](https://briarproject.org/) +## Server CLI version + +A fat JAR for running on a GNU/Linux server can be compiled with + + ./gradlew x86LinuxJar ## Donate [](https://liberapay.com/Briar/donate) [](https://flattr.com/t/592836/) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt index fce42c9512b07a41dac70fab0c703f8c3a69999e..918541561ffd28db6cb217b45024757e2e3dbd19 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/AppModule.kt @@ -45,20 +45,22 @@ import javax.inject.Singleton internal class AppModule { @Singleton @Provides - fun provideDatabaseConfig(app: Application) = object : DatabaseConfig { + fun provideDatabaseConfig(fileProvider: FileProvider) = object : DatabaseConfig { override fun getDatabaseDirectory(): File { - return app.applicationContext.getDir("db", MODE_PRIVATE) + // The database itself does mkdirs() and we use the existence to see if DB exists + return File(fileProvider.root, "db") } } @Singleton @Provides fun provideFileProvider(app: Application) = object : FileProvider { - private val tempFilesDir = File(app.applicationContext.cacheDir, "tmp").also { it.mkdirs() } + override val root: File get() = app.applicationContext.filesDir override val folderRoot = app.applicationContext.getDir("folders", MODE_PRIVATE) + private val tempFilesDir = File(app.applicationContext.cacheDir, "tmp").apply { mkdirs() } override fun getTemporaryFile(fileId: String) = File(tempFilesDir, fileId) - override fun getFolder(folderId: String) = File(folderRoot, folderId).also { it.mkdirs() } + override fun getFolder(folderId: String) = File(folderRoot, folderId).apply { mkdirs() } override fun getFile(folderId: String, fileId: String) = File(getFolder(folderId), fileId) } diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/StartReceiver.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/StartReceiver.kt index 9865d44008e9c131cd57ec6d083f8440b87ffabb..0719c1be073aea16f69d4a0766e2fe4992c68153 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/StartReceiver.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/StartReceiver.kt @@ -27,6 +27,7 @@ import android.content.Intent.ACTION_MY_PACKAGE_REPLACED import dagger.hilt.android.AndroidEntryPoint import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.NOT_STARTED +import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.util.LogUtils.debug import org.slf4j.LoggerFactory.getLogger import javax.inject.Inject @@ -39,10 +40,16 @@ class StartReceiver : BroadcastReceiver() { @Inject internal lateinit var lifecycleManager: LifecycleManager + @Inject + internal lateinit var setupManager: SetupManager + override fun onReceive(context: Context, intent: Intent) { val action = intent.action if (action != ACTION_BOOT_COMPLETED && action != ACTION_MY_PACKAGE_REPLACED) return + // don't start, if we don't even have a database + if (!setupManager.hasDb) return + val lifecycleState = lifecycleManager.lifecycleStateFlow.value LOG.debug { "Received $action in state ${lifecycleState.name}" } if (lifecycleState == NOT_STARTED) { diff --git a/mailbox-cli/build.gradle b/mailbox-cli/build.gradle index 51a8dfb078c35023282fc4bbf4fb6a3c87cc794e..b64d6ce7600a85c76f9a30425b6733806932189a 100644 --- a/mailbox-cli/build.gradle +++ b/mailbox-cli/build.gradle @@ -1,3 +1,9 @@ +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream + +import static java.util.Collections.list + plugins { id 'application' id 'idea' @@ -83,6 +89,70 @@ processResources { dependsOn unpackTorBinaries } +void jarFactory(Jar jarTask, jarArchitecture) { + jarTask.doFirst { + println 'Building ' + jarArchitecture + ' version has started' + } + jarTask.manifest { + attributes( + 'Main-Class': application.getMainClass() + ) + } + jarTask.setArchiveClassifier(jarArchitecture) + jarTask.from { + configurations.runtimeClasspath.collect { file -> + file.isDirectory() ? file : zipTree(file) + } + } { copySpec -> + copySpec.duplicatesStrategy(DuplicatesStrategy.EXCLUDE) + String[] architectures = ["linux-aarch64", "linux-armhf", "linux-x86_64"] + for (String arch : architectures) { + if (arch != jarArchitecture) { + exclude "obfs4proxy_" + arch + ".zip" + exclude "tor_" + arch + ".zip" + } + } + exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' + } + jarTask.with jar + jarTask.doLast { + // Rename the original jar + File jar = getArchiveFile().get().getAsFile() + String srcPath = jar.toString().replaceFirst('\\.jar$', '.unsorted.jar') + File srcFile = new File(srcPath) + jar.renameTo(srcFile) + JarFile srcJarFile = new JarFile(srcFile) + OutputStream destStream = new JarOutputStream(new FileOutputStream(jar)) + // Read and sort the entries + Map<String, JarEntry> entries = new TreeMap<>() + for (JarEntry e : list(srcJarFile.entries())) entries.put(e.getName(), e) + // Write the sorted entries + for (JarEntry srcEntry : entries.values()) { + JarEntry destEntry = new JarEntry(srcEntry.getName()) + destEntry.setTime(0) + destStream.putNextEntry(destEntry) + InputStream srcStream = srcJarFile.getInputStream(srcEntry) + int read + byte[] buf = new byte[4096] + while ((read = srcStream.read(buf, 0, buf.length)) != -1) { + destStream.write(buf, 0, read) + } + destStream.closeEntry() + srcStream.close() + } + destStream.close() + srcJarFile.close() + println 'Building ' + jarArchitecture + ' version has finished' + println 'JAR: mailbox-cli/build/libs/mailbox-cli-linux-x86_64.jar' + } +} + +task x86LinuxJar(type: Jar) { + group = "Build" + description = "Assembles a runnable fat jar for x86-64 Linux" + jarFactory(it, 'linux-x86_64') +} + tasks.withType(Test) { systemProperty 'java.library.path', 'libs' } diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt index 58774946a9b32cbb472412908c022c1fc6a0d32b..1e90dcadd2f9929bea369ae47a054aeb263a2685 100644 --- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/JavaCliModule.kt @@ -61,6 +61,11 @@ internal class JavaCliModule { private const val DATAHOME_SUBDIR = "briar-mailbox" } + /** + * Returns the [File] for the data directory of the mailbox. + * If XDG_DATA_HOME is defined, it returns "$XDG_DATA_HOME/briar-mailbox" + * and otherwise it returns "~/.local/share/briar-mailbox". + */ private val dataDir: File by lazy { val dataHome = when (val custom = System.getenv("XDG_DATA_HOME").orEmpty()) { "" -> File(DEFAULT_DATAHOME) @@ -89,26 +94,22 @@ internal class JavaCliModule { @Singleton @Provides - fun provideDatabaseConfig() = object : DatabaseConfig { + fun provideDatabaseConfig(fileProvider: FileProvider) = object : DatabaseConfig { override fun getDatabaseDirectory(): File { - val dbDir = File(dataDir, "db") - if (!dbDir.exists() && !dbDir.mkdirs()) { - throw IOException("dbDir could not be created: ${dbDir.absolutePath}") - } else if (!dbDir.isDirectory) { - throw IOException("dbDir is not a directory: ${dbDir.absolutePath}") - } - return dbDir + // The database itself does mkdirs() and we use the existence to see if DB exists + return File(fileProvider.root, "db") } } @Singleton @Provides fun provideFileProvider() = object : FileProvider { - private val tempFilesDir = File(dataDir, "tmp").also { it.mkdirs() } - override val folderRoot = File(dataDir, "folders").also { it.mkdirs() } + override val root: File get() = dataDir + private val tempFilesDir = File(dataDir, "tmp").apply { mkdirs() } + override val folderRoot = File(dataDir, "folders").apply { mkdirs() } override fun getTemporaryFile(fileId: String) = File(tempFilesDir, fileId) - override fun getFolder(folderId: String) = File(folderRoot, folderId).also { it.mkdirs() } + override fun getFolder(folderId: String) = File(folderRoot, folderId).apply { mkdirs() } override fun getFile(folderId: String, fileId: String) = File(getFolder(folderId), fileId) } diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt index 44cf1c5ed05f073620b16a7a257481b6c2820198..1c0c0514fdc0510a6a566e491bbd8543e69f47e2 100644 --- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt +++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt @@ -25,13 +25,17 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.counted import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.CoreEagerSingletons import org.briarproject.mailbox.core.JavaCliEagerSingletons import org.briarproject.mailbox.core.db.TransactionManager import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.setup.QrCodeEncoder import org.briarproject.mailbox.core.setup.SetupManager +import org.briarproject.mailbox.core.setup.WipeManager import org.briarproject.mailbox.core.system.InvalidIdException +import org.briarproject.mailbox.core.tor.TorPlugin import org.slf4j.LoggerFactory.getLogger import java.util.logging.Level.ALL import java.util.logging.Level.INFO @@ -44,6 +48,10 @@ class Main : CliktCommand( name = "briar-mailbox", help = "Command line interface for the Briar Mailbox" ) { + private val wipe by option( + "--wipe", + help = "Deletes entire mailbox, will require new setup", + ).flag(default = false) private val debug by option("--debug", "-d", help = "Enable printing of debug messages").flag( default = false ) @@ -69,6 +77,12 @@ class Main : CliktCommand( @Inject internal lateinit var setupManager: SetupManager + @Inject + internal lateinit var wipeManager: WipeManager + + @Inject + internal lateinit var torPlugin: TorPlugin + @Inject internal lateinit var qrCodeEncoder: QrCodeEncoder @@ -93,6 +107,15 @@ class Main : CliktCommand( val javaCliComponent = DaggerJavaCliComponent.builder().build() javaCliComponent.inject(this) + if (wipe) { + wipeManager.wipeFilesOnly() + println("Mailbox wiped successfully \\o/") + exitProcess(0) + } + startLifecycle() + } + + private fun startLifecycle() { Runtime.getRuntime().addShutdownHook( Thread { lifecycleManager.stopServices() @@ -103,23 +126,36 @@ class Main : CliktCommand( lifecycleManager.startServices() lifecycleManager.waitForStartup() - if (setupToken != null) try { - setupManager.setToken(setupToken, null) - } catch (e: InvalidIdException) { - System.err.println("Invalid setup token") - exitProcess(1) + if (setupToken != null) { + try { + setupManager.setToken(setupToken, null) + } catch (e: InvalidIdException) { + System.err.println("Invalid setup token") + exitProcess(1) + } } - // TODO this is obviously not the final code, just a stub to get us started - val setupTokenExists = db.read { txn -> - setupManager.getSetupToken(txn) != null - } val ownerTokenExists = db.read { txn -> setupManager.getOwnerToken(txn) != null } - if (!setupTokenExists && !ownerTokenExists) setupManager.restartSetup() - qrCodeEncoder.getQrCodeBitMatrix()?.let { - println(QrCodeRenderer.getQrString(it)) + if (!ownerTokenExists) { + if (debug) { + val token = setupToken ?: db.read { setupManager.getSetupToken(it) } + println( + "curl -v -H \"Authorization: Bearer $token\" -X PUT " + + "http://localhost:8000/setup" + ) + } + // If not set up, show QR code for manual setup + runBlocking { + // wait until Tor becomes active and published the onion service + torPlugin.state.takeWhile { state -> + state != TorPlugin.State.ACTIVE + } + } + qrCodeEncoder.getQrCodeBitMatrix()?.let { + println(QrCodeRenderer.getQrString(it)) + } } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt index 31dce9ca6947a43adfccaa3d6a91619ba62da8af..631bfebf1a2b6559c73761f963e50b26c10099e5 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/CoreModule.kt @@ -28,6 +28,7 @@ import org.briarproject.mailbox.core.event.EventModule import org.briarproject.mailbox.core.lifecycle.LifecycleModule import org.briarproject.mailbox.core.server.WebServerModule import org.briarproject.mailbox.core.settings.SettingsModule +import org.briarproject.mailbox.core.setup.SetupModule import org.briarproject.mailbox.core.system.Clock import org.briarproject.mailbox.core.tor.TorModule import javax.inject.Singleton @@ -37,6 +38,7 @@ import javax.inject.Singleton EventModule::class, LifecycleModule::class, DatabaseModule::class, + SetupModule::class, WebServerModule::class, SettingsModule::class, TorModule::class, 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 index dc113ea23a3d4c46047d6147f2b2bc426b377e5e..8cc0869936a2c50288b8afb435c344174a1c151d 100644 --- 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 @@ -28,9 +28,12 @@ import io.ktor.response.respondFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.db.DatabaseConfig 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.setup.SetupManager +import org.briarproject.mailbox.core.setup.WipeManager import org.briarproject.mailbox.core.system.InvalidIdException import org.briarproject.mailbox.core.system.RandomIdManager import org.slf4j.LoggerFactory.getLogger @@ -40,10 +43,23 @@ private val LOG = getLogger(FileManager::class.java) class FileManager @Inject constructor( private val fileProvider: FileProvider, + private val dbConfig: DatabaseConfig, ) { + + /** + * Used by [SetupManager] to test for the existence of the database. + */ + fun hasDbFile(): Boolean { + val dbDir = dbConfig.getDatabaseDirectory() + return dbDir.isDirectory + } + + /** + * Used by [WipeManager] to wipe all files. + */ fun deleteAllFiles(): Boolean { var allDeleted = true - fileProvider.folderRoot.listFiles()?.forEach { folder -> + fileProvider.root.listFiles()?.forEach { folder -> if (!folder.deleteRecursively()) { allDeleted = false LOG.warn("Not everything in $folder could get deleted.") diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileProvider.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileProvider.kt index b8a10f5ca19268d10b931580cd80f9d12ca3ac32..660a790b93060a8c4d546731b19636b78b879f2c 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileProvider.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileProvider.kt @@ -22,6 +22,14 @@ package org.briarproject.mailbox.core.files import java.io.File interface FileProvider { + /** + * The root files directory. + * Attention: This is not guaranteed to be the parent of other files on all platforms. + * Also this directory and all of its content are deleted during wipe, + * so make sure this is a directory where this doesn't do any harm, + * i.e. a directory used for the mailbox only. + */ + val root: File val folderRoot: File fun getTemporaryFile(fileId: String): File fun getFolder(folderId: String): File diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt index d7ab1ea2c916015a349fbe2f4759e825d9794d11..2b631bfb16cec4ac91567e81501a574a8c69de0b 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManager.kt @@ -41,4 +41,11 @@ interface SettingsManager { */ @Throws(DbException::class) fun mergeSettings(s: Settings, namespace: String) + + /** + * Merges the given settings with any existing settings in the given + * namespace. + */ + @Throws(DbException::class) + fun mergeSettings(txn: Transaction, s: Settings, namespace: String) } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt index 49930785522aae6c61dbfc03482a462b0221bdea..a8e404ebeca97c86917ae59c77e59f26a6a1e598 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt @@ -43,4 +43,8 @@ internal class SettingsManagerImpl(private val db: Database) : SettingsManager { override fun mergeSettings(s: Settings, namespace: String) { db.write { txn -> db.mergeSettings(txn, s, namespace) } } + + override fun mergeSettings(txn: Transaction, s: Settings, namespace: String) { + db.mergeSettings(txn, s, namespace) + } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt index c74d1fac3fba7e77b253d48935d122a1a58b4216..faccb6fdde94d89dff9cfccee8c0e0901dc98006 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt @@ -25,6 +25,8 @@ import io.ktor.http.HttpStatusCode import io.ktor.response.respond import org.briarproject.mailbox.core.db.DbException import org.briarproject.mailbox.core.db.Transaction +import org.briarproject.mailbox.core.files.FileManager +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.OpenDatabaseHook import org.briarproject.mailbox.core.server.AuthException import org.briarproject.mailbox.core.server.AuthManager import org.briarproject.mailbox.core.settings.Settings @@ -36,23 +38,52 @@ private const val SETTINGS_NAMESPACE_OWNER = "owner" private const val SETTINGS_SETUP_TOKEN = "setupToken" private const val SETTINGS_OWNER_TOKEN = "ownerToken" -class SetupManager @Inject constructor( +interface SetupManager : OpenDatabaseHook { + /** + * True if a database has been setup. + * This is usually the case, if the lifecycle has been started once. + * The Mailbox might still need pairing/linking. + * This is false after wiping. + */ + val hasDb: Boolean + + @Throws(DbException::class) + fun setToken(setupToken: String?, ownerToken: String?) + + @Throws(DbException::class) + fun getSetupToken(txn: Transaction): String? + + @Throws(DbException::class) + fun getOwnerToken(txn: Transaction): String? +} + +class SetupManagerImpl @Inject constructor( private val randomIdManager: RandomIdManager, private val settingsManager: SettingsManager, -) { + private val fileManager: FileManager, +) : SetupManager { - /** - * Stores a new single-use setup token and wipes the owner auth token, if one existed. - */ - fun restartSetup() { - val settings = Settings() - settings[SETTINGS_SETUP_TOKEN] = randomIdManager.getNewRandomId() - settings[SETTINGS_OWNER_TOKEN] = null - settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER) + override val hasDb: Boolean get() = fileManager.hasDbFile() + + @Throws(DbException::class) + override fun onDatabaseOpened(txn: Transaction) { + val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) + val setupToken = settings[SETTINGS_SETUP_TOKEN] + val ownerToken = settings[SETTINGS_OWNER_TOKEN] + // ensure that setup token is initialized if both tokens are empty + if (setupToken == null && ownerToken == null) { + settings[SETTINGS_SETUP_TOKEN] = randomIdManager.getNewRandomId() + settingsManager.mergeSettings(txn, settings, SETTINGS_NAMESPACE_OWNER) + } } + /** + * Sets either the [setupToken] or the [ownerToken]. + * Can not set both at once. + */ @Throws(DbException::class) - fun setToken(setupToken: String?, ownerToken: String?) { + override fun setToken(setupToken: String?, ownerToken: String?) { + require(setupToken == null || ownerToken == null) { "Can not set both tokens" } val settings = Settings() if (setupToken != null) randomIdManager.assertIsRandomId(setupToken) settings[SETTINGS_SETUP_TOKEN] = setupToken @@ -61,13 +92,14 @@ class SetupManager @Inject constructor( settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER) } - fun getSetupToken(txn: Transaction): String? { + @Throws(DbException::class) + override fun getSetupToken(txn: Transaction): String? { val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) return settings[SETTINGS_SETUP_TOKEN] } @Throws(DbException::class) - fun getOwnerToken(txn: Transaction): String? { + override fun getOwnerToken(txn: Transaction): String? { val settings = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER) return settings[SETTINGS_OWNER_TOKEN] } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..55b9fe2fdedc497ed9e3530d2f3953ae369c7715 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupModule.kt @@ -0,0 +1,39 @@ +/* + * Briar Mailbox + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +package org.briarproject.mailbox.core.setup + +import dagger.Module +import dagger.Provides +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import javax.inject.Singleton + +@Module +class SetupModule { + @Provides + @Singleton + fun provideSetupManager( + lifecycleManager: LifecycleManager, + setupManagerImpl: SetupManagerImpl, + ): SetupManager { + return setupManagerImpl.also { + lifecycleManager.registerOpenDatabaseHook(it) + } + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt index 59629b3cd8459a2a03b562559ce2f84f13dba434..7cefaaebb256539775eea64cf40606656dcf1b72 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt @@ -24,33 +24,33 @@ 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.db.DatabaseConfig import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.server.AuthException import org.briarproject.mailbox.core.server.MailboxPrincipal import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal -import org.briarproject.mailbox.core.util.IoUtils import javax.inject.Inject class WipeManager @Inject constructor( private val db: Database, - private val databaseConfig: DatabaseConfig, private val fileManager: FileManager, ) { - /* - * This must only be called by the LifecycleManager + /** + * Drops database tables and then deletes all files, includes the database files. + * + * This must only be called by the [LifecycleManager]. */ fun wipeDatabaseAndFiles() { db.dropAllTablesAndClose() - val dir = databaseConfig.getDatabaseDirectory() - IoUtils.deleteFileOrDir(dir) fileManager.deleteAllFiles() } - /* - * This must only be called by the LifecycleManager + /** + * Deletes all files, includes the database files. + * + * This must only be called by the [LifecycleManager] + * or by the CLI when no lifecycle was started. */ fun wipeFilesOnly() { fileManager.deleteAllFiles() diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java index ab2bbd79673fdc25445e7dbae238bd415e5d5426..73f881b47dfd9e931d5fc0b0db9809e0c6d0a59e 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java @@ -59,10 +59,14 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; + import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.Objects.requireNonNull; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY; import static org.briarproject.mailbox.core.tor.TorConstants.CONTROL_PORT; @@ -70,7 +74,6 @@ import static org.briarproject.mailbox.core.tor.TorConstants.HS_ADDRESS_V3; import static org.briarproject.mailbox.core.tor.TorConstants.HS_PRIVATE_KEY_V3; import static org.briarproject.mailbox.core.tor.TorConstants.SETTINGS_NAMESPACE; import static org.briarproject.mailbox.core.tor.TorPlugin.State.ACTIVE; -import static org.briarproject.mailbox.core.tor.TorPlugin.State.DISABLED; import static org.briarproject.mailbox.core.tor.TorPlugin.State.ENABLING; import static org.briarproject.mailbox.core.tor.TorPlugin.State.INACTIVE; import static org.briarproject.mailbox.core.tor.TorPlugin.State.STARTING_STOPPING; @@ -155,6 +158,10 @@ public abstract class TorPlugin return new File(torDirectory, "obfs4proxy"); } + public StateFlow<State> getState() { + return state.state; + } + @Override public void startService() throws ServiceException { if (used.getAndSet(true)) throw new IllegalStateException(); @@ -404,6 +411,7 @@ public abstract class TorPlugin logException(LOG, e); } } + state.setServicePublished(); } @Nullable @@ -534,7 +542,6 @@ public abstract class TorPlugin else LOG.info("Country code: " + country); } - int reasonsDisabled = 0; boolean enableNetwork = false; boolean enableBridges = false; boolean useMeek = false; @@ -557,7 +564,6 @@ public abstract class TorPlugin LOG.info("Not using bridges"); } } - state.setReasonsDisabled(reasonsDisabled); try { if (enableNetwork) { enableBridges(enableBridges, useMeek); @@ -581,7 +587,10 @@ public abstract class TorPlugin } @ThreadSafe - protected class PluginState { + protected static class PluginState { + + private final MutableStateFlow<State> state = + MutableStateFlow(STARTING_STOPPING); @GuardedBy("this") private boolean started = false, @@ -590,10 +599,7 @@ public abstract class TorPlugin networkEnabled = false, bootstrapped = false, circuitBuilt = false, - settingsChecked = false; - - @GuardedBy("this") - private int reasonsDisabled = 0; + servicePublished = false; @GuardedBy("this") @Nullable @@ -601,7 +607,7 @@ public abstract class TorPlugin synchronized void setStarted() { started = true; -// callback.pluginStateChanged(getState()); + state.setValue(getCurrentState()); } synchronized boolean isTorRunning() { @@ -613,63 +619,52 @@ public abstract class TorPlugin stopped = true; ServerSocket ss = serverSocket; serverSocket = null; -// callback.pluginStateChanged(getState()); + state.setValue(getCurrentState()); return ss; } synchronized void setBootstrapped() { bootstrapped = true; -// callback.pluginStateChanged(getState()); + state.setValue(getCurrentState()); } synchronized boolean getAndSetCircuitBuilt() { boolean firstCircuit = !circuitBuilt; circuitBuilt = true; -// callback.pluginStateChanged(getState()); + state.setValue(getCurrentState()); return firstCircuit; } + synchronized void setServicePublished() { + servicePublished = true; + state.setValue(getCurrentState()); + } + synchronized void enableNetwork(boolean enable) { networkInitialised = true; networkEnabled = enable; if (!enable) circuitBuilt = false; -// callback.pluginStateChanged(getState()); + state.setValue(getCurrentState()); } - synchronized void setReasonsDisabled(int reasonsDisabled) { - settingsChecked = true; - this.reasonsDisabled = reasonsDisabled; -// callback.pluginStateChanged(getState()); - } - - synchronized State getState() { - if (!started || stopped || !settingsChecked) { + private synchronized State getCurrentState() { + if (!started || stopped) { return STARTING_STOPPING; } - if (reasonsDisabled != 0) return DISABLED; if (!networkInitialised) return ENABLING; if (!networkEnabled) return INACTIVE; - return bootstrapped && circuitBuilt ? ACTIVE : ENABLING; - } - - synchronized int getReasonsDisabled() { - return getState() == DISABLED ? reasonsDisabled : 0; + return bootstrapped && circuitBuilt && servicePublished ? + ACTIVE : ENABLING; } } - enum State { - + public enum State { /** * The plugin has not finished starting or has been stopped. */ STARTING_STOPPING, - /** - * The plugin is disabled by settings. - */ - DISABLED, - /** * The plugin is being enabled and can't yet make or receive * connections. diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt index e0ad498eb2764b4fd53066fb45a2cbf2f7feda97..960874cd8156d7c84da414d9c281237a3510881a 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt @@ -9,7 +9,7 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.settings.MetadataManager import org.briarproject.mailbox.core.settings.SettingsManager import org.briarproject.mailbox.core.setup.SetupManager -import org.briarproject.mailbox.core.system.RandomIdManager +import org.briarproject.mailbox.core.setup.WipeManager import javax.inject.Singleton @Singleton @@ -26,7 +26,7 @@ interface TestComponent { fun getFileManager(): FileManager fun getDatabaseConfig(): DatabaseConfig fun getDatabase(): Database - fun getRandomIdManager(): RandomIdManager fun getFileProvider(): FileProvider fun getMetadataManager(): MetadataManager + fun getWipeManager(): WipeManager } 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 2a1adc06bdf8bc09bb97f6dd91fae266dd1cdf42..5ac0c7dfe6ef609fee33651b68b7d308457b7257 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 @@ -10,6 +10,7 @@ import org.briarproject.mailbox.core.files.FileProvider import org.briarproject.mailbox.core.lifecycle.LifecycleModule import org.briarproject.mailbox.core.server.WebServerModule import org.briarproject.mailbox.core.settings.SettingsModule +import org.briarproject.mailbox.core.setup.SetupModule import org.briarproject.mailbox.core.system.Clock import java.io.File import javax.inject.Singleton @@ -18,6 +19,7 @@ import javax.inject.Singleton includes = [ LifecycleModule::class, TestDatabaseModule::class, + SetupModule::class, WebServerModule::class, SettingsModule::class, // no Tor module @@ -40,11 +42,16 @@ internal class TestModule(private val tempDir: File) { @Singleton @Provides fun provideFileProvider() = object : FileProvider { - private val tempFilesDir = File(tempDir, "tmp").also { it.mkdirs() } + override val root: File get() = tempDir override val folderRoot = File(tempDir, "folders") + private val tempFilesDir = File(tempDir, "tmp").apply { mkdirs() } - override fun getTemporaryFile(fileId: String) = File(tempFilesDir, fileId) - override fun getFolder(folderId: String) = File(folderRoot, folderId).also { it.mkdirs() } + override fun getTemporaryFile(fileId: String) = File(tempFilesDir, fileId).apply { + // we delete root at the end of each test, so tempFilesDir gets deleted as well + parentFile.mkdirs() + } + + override fun getFolder(folderId: String) = File(folderRoot, folderId).apply { mkdirs() } override fun getFile(folderId: String, fileId: String) = File(getFolder(folderId), fileId) } } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt index de2f1d0461bd0ded29e70e91c0a353160794caec..fd0d67b97374bf1a1ce6a4865ea2cbe3fa801430 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.TestUtils.assertTimestampRecent import org.briarproject.mailbox.core.TestUtils.getNewRandomId import org.briarproject.mailbox.core.server.IntegrationTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -31,12 +30,6 @@ class FileManagerIntegrationTest : IntegrationTest() { addContact(contact2) } - @AfterEach - override fun clearDb() { - super.clearDb() - testComponent.getFileManager().deleteAllFiles() - } - @Test fun `post new file rejects wrong token`(): Unit = runBlocking { val response: HttpResponse = httpClient.post("$baseUrl/files/${getNewRandomId()}") { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt index 10afd983ebd1403e75c3a426035ff35d0a80efdb..bbb9f151b9da66d78110d5b3687f2be17be3807c 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt @@ -24,6 +24,8 @@ import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.io.TempDir import java.io.File +import kotlin.test.assertFalse +import kotlin.test.assertTrue @TestInstance(Lifecycle.PER_CLASS) abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { @@ -31,7 +33,9 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { protected lateinit var testComponent: TestComponent protected val db by lazy { testComponent.getDatabase() } private val lifecycleManager by lazy { testComponent.getLifecycleManager() } + protected val setupManager by lazy { testComponent.getSetupManager() } protected val metadataManager by lazy { testComponent.getMetadataManager() } + private val wipeManager by lazy { testComponent.getWipeManager() } protected val httpClient = HttpClient(CIO) { expectSuccess = false // prevents exceptions on non-success responses if (installJsonFeature) { @@ -55,6 +59,7 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { fun setUp(@TempDir tempDir: File) { testComponent = DaggerTestComponent.builder().testModule(TestModule(tempDir)).build() testComponent.injectCoreEagerSingletons() + assertFalse(setupManager.hasDb) lifecycleManager.startServices() lifecycleManager.waitForStartup() } @@ -73,11 +78,13 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { // clears [metadataManager.ownerConnectionTime] metadataManager.onDatabaseOpened(txn) } + assertTrue(setupManager.hasDb) } @AfterEach - open fun clearDb() { - db.dropAllTablesAndClose() + open fun wipe() { + wipeManager.wipeDatabaseAndFiles() + assertFalse(setupManager.hasDb) } protected fun addOwnerToken() { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt index 83e40b887e92326ca36b829f853641d6726cbe9a..86f28ca64de352f0761dcffd1f02c41a56a0c85f 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt @@ -7,38 +7,10 @@ import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.server.IntegrationTest import org.junit.jupiter.api.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull import kotlin.test.assertNull class SetupManagerTest : IntegrationTest() { - private val setupManager by lazy { testComponent.getSetupManager() } - - @Test - fun `restarting setup wipes owner token and creates setup token`() { - // initially, there's no setup and no owner token - db.read { txn -> - assertNull(setupManager.getSetupToken(txn)) - assertNull(setupManager.getOwnerToken(txn)) - } - - // setting an owner token stores it in DB - setupManager.setToken(null, ownerToken) - db.read { txn -> - assertNull(setupManager.getSetupToken(txn)) - assertEquals(ownerToken, setupManager.getOwnerToken(txn)) - } - - // restarting setup wipes owner token, creates setup token - setupManager.restartSetup() - db.read { txn -> - val setupToken = setupManager.getSetupToken(txn) - assertNotNull(setupToken) - testComponent.getRandomIdManager().assertIsRandomId(setupToken) - assertNull(setupManager.getOwnerToken(txn)) - } - } - @Test fun `setup request gets rejected when using non-setup token`() = runBlocking { // initially, there's no setup and no owner token @@ -78,6 +50,9 @@ class SetupManagerTest : IntegrationTest() { // set a setup-token setupManager.setToken(token, null) + // we are not yet set up + assertNull(db.read { txn -> setupManager.getOwnerToken(txn) }) + // use it for setup PUT request val response: SetupResponse = httpClient.put("$baseUrl/setup") { authenticateWithToken(token) diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest.kt index 984af156d0059376039d3ce0b0d83397210c3857..d29fd75bae37f94104c8088ee9ec1d754a872c9a 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest.kt @@ -41,7 +41,11 @@ class WipingWipeRouteManagerTest : IntegrationTest() { // no more files are stored val folderRoot = testComponent.getFileProvider().folderRoot - assertTrue(folderRoot.listFiles()?.isEmpty() ?: false) + assertFalse(folderRoot.exists()) + + // file root has been cleared + val root = testComponent.getFileProvider().root + assertTrue(root.listFiles()?.isEmpty() ?: false) // no more contacts in DB - contacts table is gone // it actually fails because db is closed though