Commit a030f922 authored by akwizgran's avatar akwizgran

Merge branch 'headless' into 'master'

Add Briar headless client that exposes a REST API

See merge request briar/briar!931
parents 8a15fb24 b3615b4a
......@@ -36,6 +36,9 @@
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
......@@ -257,5 +260,11 @@
<codeStyleSettings language="kotlin">
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="1" />
\ No newline at end of file
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All in briar-headless" type="AndroidJUnit" factoryName="Android JUnit" nameIsGenerated="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<module name="briar-headless" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="" />
<option name="PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$/briar-headless" />
<option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" />
<envs />
<patterns />
<method />
\ No newline at end of file
......@@ -24,6 +24,7 @@
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All tests in bramble-android" run_configuration_type="AndroidJUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All tests in bramble-java" run_configuration_type="AndroidJUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All tests in briar-core" run_configuration_type="AndroidJUnit" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="All in briar-headless" run_configuration_type="AndroidJUnit" />
\ No newline at end of file
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="briar-headless" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="-v" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="org.briarproject.briar.headless.MainKt" />
<option name="WORKING_DIRECTORY" value="" />
<module name="briar-headless" />
<envs />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="jar" externalProjectPath="$PROJECT_DIR$/briar-headless" vmOptions="" scriptParameters="" />
\ No newline at end of file
# Briar REST API
This is a headless Briar peer that exposes a REST API
with an integrated HTTP server instead of a traditional user interface.
You can use this API to script the peer behavior
or to develop your own user interface for it.
## How to use
The REST API peer comes as a `jar` file
and needs a Java Runtime Environment (JRE) that supports at least Java 8.
It currently works only on GNU/Linux operating systems.
You can start the peer (and its API server) like this:
$ java -jar briar-headless/build/libs/briar-headless.jar
It is possible to put parameters at the end.
Try `--help` for a list of options.
On the first start, it will ask you to create a user account:
$ java -jar briar-headless.jar
No account found. Let's create one!
Nickname: testuser
After entering a password, it will start up without further output.
Use the `-v` option if you prefer more verbose logging.
By default, Briar creates a folder `~/.briar` where it stores its database and other files.
There you also find the authentication token which is required to interact with the API:
$ cat ~/.briar/auth_token
You can test that things work as expected by running:
$ curl -H "Authorization: Bearer DZbfoUie8sjap7CSDR9y6cgJCojV+xUITTIFbgtAgqk="
The answer is an empty JSON array, because you don't have any contacts.
Note that the HTTP request sets an `Authorization` header with the bearer token.
A missing or wrong token will result in a `401` response.
### Listing all contacts
`GET /v1/contacts`
Returns a JSON array of contacts:
"author": {
"formatVersion": 1,
"id": "y1wkIzAimAbYoCGgWxkWlr6vnq1F8t1QRA/UMPgI0E0=",
"name": "Test",
"publicKey": "BDu6h1S02bF4W6rgoZfZ6BMjTj/9S9hNN7EQoV05qUo="
"contactId": 1,
"verified": true
### Adding a contact
*Not yet implemented*
The only workaround is to add a contact to the Briar app running on a rooted Android phone
and then move its database (and key files) to the headless peer.
### Listing all private messages
`GET /messages/{contactId}`
The `{contactId}` is the `contactId` of the contact (`1` in the example above).
It returns a JSON array of private messages:
"contactId": 1,
"groupId": "oRRvCri85UE2XGcSloAKt/u8JDcMkmDc26SOMouxr4U=",
"id": "ZGDrlpCxO9v7doO4Bmijh95QqQDykaS4Oji/mZVMIJ8=",
"local": true,
"read": true,
"seen": true,
"sent": true,
"text": "test",
"timestamp": 1537376633850,
"type": "PrivateMessage"
If `local` is `true`, the message was sent by the Briar peer instead of its remote contact.
Attention: There can messages of other `type`s where the message `text` is `null`.
### Writing a private message
`POST /messages/{contactId}`
The text of the message should be posted as JSON:
"text": "Hello World!"
### Listing blog posts
`GET /v1/blogs/posts`
Returns a JSON array of blog posts:
"author": {
"formatVersion": 1,
"id": "VNKXkaERPpXmZuFbHHwYT6Qc148D+KNNxQ4hwtx7Kq4=",
"name": "Test",
"publicKey": "NbwpQWjS3gHMjjDQIASIy/j+bU6NRZnSRT8X8FKDoN4="
"authorStatus": "ourselves",
"id": "X1jmHaYfrX47kT5OEd0OD+p/bptyR92IvuOBYSgxETM=",
"parentId": null,
"read": true,
"rssFeed": false,
"text": "Test Post Content",
"timestamp": 1535397886749,
"timestampReceived": 1535397886749,
"type": "post"
### Writing a blog post
`POST /v1/blogs/posts`
The text of the blog post should be posted as JSON:
"text": "Hello Blog World!"
## Websocket API
The Briar peer uses a websocket to notify a connected API client about new events.
`WS /v1/ws`
The websocket request must use basic auth,
with the authentication token as the username and a blank password.
You can test connecting to the websocket with curl:
$ curl --no-buffer \
--header "Connection: Upgrade" \
--header "Upgrade: websocket" \
--header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
--header "Sec-WebSocket-Version: 13" \
The headers are only required when testing with curl.
Your websocket client will most likely add these headers automatically.
### Receiving new private messages
When the Briar peer receives a new private message,
it will send a JSON object to connected websocket clients:
"data": {
"contactId": 1,
"groupId": "oRRvCri85UE2XGcSloAKt/u8JDcMkmDc26SOMouxr4U=",
"id": "JBc+ogQIok/yr+7XtxN2iQgNfzw635mHikNaP5QOEVs=",
"local": false,
"read": false,
"seen": false,
"sent": false,
"text": "Test Message",
"timestamp": 1537389146088,
"type": "PrivateMessage"
"name": "PrivateMessageReceivedEvent",
"type": "event"
Note that the JSON object in `data` is exactly what the REST API returns
when listing private messages.
plugins {
id 'java'
id 'idea'
id 'org.jetbrains.kotlin.jvm' version '1.2.70'
id 'org.jetbrains.kotlin.kapt' version '1.2.70'
id 'witness'
apply from: 'witness.gradle'
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
implementation project(path: ':briar-core', configuration: 'default')
implementation project(path: ':bramble-java', configuration: 'default')
implementation 'io.javalin:javalin:2.2.0'
implementation 'org.slf4j:slf4j-simple:1.7.25'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.6'
implementation 'com.github.ajalt:clikt:1.5.0'
kapt ''
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation project(path: ':bramble-core', configuration: 'testOutput')
def junitVersion = '5.2.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "io.mockk:mockk:1.8.6"
testImplementation "org.skyscreamer:jsonassert:1.5.0"
jar {
manifest {
'Main-Class': 'org.briarproject.briar.headless.MainKt'
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
// At the moment for non-Android projects we need to explicitly mark the code generated by kapt
// as 'generated source code' for correct highlighting and resolve in IDE.
idea {
module {
sourceDirs += file('build/generated/source/kapt/main')
generatedSourceDirs += file('build/generated/source/kapt/main')
test {
testLogging {
events "passed", "skipped", "failed"
package org.briarproject.bramble.identity
import org.briarproject.bramble.api.identity.Author
import org.briarproject.briar.headless.json.JsonDict
fun Author.output() = JsonDict(
"formatVersion" to formatVersion,
"id" to id.bytes,
"name" to name,
"publicKey" to publicKey
fun Author.Status.output() = name.toLowerCase()
package org.briarproject.briar.headless
import dagger.Component
import org.briarproject.bramble.BrambleCoreEagerSingletons
import org.briarproject.bramble.BrambleCoreModule
import org.briarproject.bramble.account.AccountModule
import org.briarproject.bramble.system.DesktopSecureRandomModule
import org.briarproject.briar.BriarCoreEagerSingletons
import org.briarproject.briar.BriarCoreModule
import javax.inject.Singleton
modules = [
internal interface BriarHeadlessApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons {
fun getRouter(): Router
fun getSecureRandom(): SecureRandom
package org.briarproject.briar.headless
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.TermUi.echo
import com.github.ajalt.clikt.output.TermUi.prompt
import org.briarproject.bramble.api.account.AccountManager
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
import org.briarproject.bramble.api.lifecycle.LifecycleManager
import java.lang.System.exit
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
internal class BriarService
private val accountManager: AccountManager,
private val lifecycleManager: LifecycleManager,
private val passwordStrengthEstimator: PasswordStrengthEstimator
) {
fun start() {
if (!accountManager.accountExists()) {
} else {
val password = prompt("Password", hideInput = true)
?: throw UsageError("Could not get password. Is STDIN connected?")
if (!accountManager.signIn(password)) {
echo("Error: Password invalid")
val dbKey = accountManager.databaseKey ?: throw AssertionError()
fun stop() {
private fun createAccount() {
echo("No account found. Let's create one!\n\n")
val nickname = prompt("Nickname") { nickname ->
if (nickname.length > MAX_AUTHOR_NAME_LENGTH)
throw UsageError("Please choose a shorter nickname!")
val password =
prompt("Password", hideInput = true, requireConfirmation = true) { password ->
if (passwordStrengthEstimator.estimateStrength(password) < QUITE_WEAK)
throw UsageError("Please enter a stronger password!")
if (nickname == null || password == null)
throw UsageError("Could not get account information. Is STDIN connected?")
accountManager.createAccount(nickname, password)
package org.briarproject.briar.headless
import org.briarproject.bramble.api.db.DatabaseConfig
import java.lang.Long.MAX_VALUE
internal class HeadlessDatabaseConfig(private val dbDir: File, private val keyDir: File) :
DatabaseConfig {
override fun getDatabaseDirectory(): File {
return dbDir
override fun getDatabaseKeyDirectory(): File {
return keyDir
override fun getMaxSize(): Long {
return MAX_VALUE
package org.briarproject.briar.headless
import dagger.Module
import dagger.Provides
import org.briarproject.bramble.api.crypto.CryptoComponent
import org.briarproject.bramble.api.crypto.PublicKey
import org.briarproject.bramble.api.db.DatabaseConfig
import org.briarproject.bramble.api.event.EventBus
import org.briarproject.bramble.api.lifecycle.IoExecutor
import org.briarproject.bramble.api.plugin.BackoffFactory
import org.briarproject.bramble.api.plugin.PluginConfig
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory
import org.briarproject.bramble.api.reporting.DevConfig
import org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS
import org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.api.system.LocationUtils
import org.briarproject.bramble.api.system.ResourceProvider
import org.briarproject.bramble.plugin.tor.CircumventionModule
import org.briarproject.bramble.plugin.tor.CircumventionProvider
import org.briarproject.bramble.plugin.tor.LinuxTorPluginFactory
import org.briarproject.bramble.system.JavaSystemModule
import org.briarproject.bramble.util.StringUtils.fromHexString
import org.briarproject.briar.headless.blogs.HeadlessBlogModule
import org.briarproject.briar.headless.event.HeadlessEventModule
import org.briarproject.briar.headless.forums.HeadlessForumModule
import org.briarproject.briar.headless.messaging.HeadlessMessagingModule
import java.util.Collections.emptyList
import java.util.concurrent.Executor
import javax.inject.Singleton
includes = [
internal class HeadlessModule(private val appDir: File) {
internal fun provideDatabaseConfig(): DatabaseConfig {
val dbDir = File(appDir, "db")
val keyDir = File(appDir, "key")
return HeadlessDatabaseConfig(dbDir, keyDir)
internal fun providePluginConfig(
@IoExecutor ioExecutor: Executor, torSocketFactory: SocketFactory,
backoffFactory: BackoffFactory, networkManager: NetworkManager,
locationUtils: LocationUtils, eventBus: EventBus,
resourceProvider: ResourceProvider,
circumventionProvider: CircumventionProvider, clock: Clock
): PluginConfig {
val torDirectory = File(appDir, "tor")
val tor = LinuxTorPluginFactory(
networkManager, locationUtils, eventBus, torSocketFactory,
backoffFactory, resourceProvider, circumventionProvider, clock,
val duplex = listOf<DuplexPluginFactory>(tor)
return object : PluginConfig {
override fun getDuplexFactories(): Collection<DuplexPluginFactory> {
return duplex
override fun getSimplexFactories(): Collection<SimplexPluginFactory> {
return emptyList()
override fun shouldPoll(): Boolean {
return true
internal fun provideDevConfig(crypto: CryptoComponent): DevConfig {
return object : DevConfig {
override fun getDevPublicKey(): PublicKey {
try {
return crypto.messageKeyParser
} catch (e: GeneralSecurityException) {
throw RuntimeException(e)
override fun getDevOnionAddress(): String {
override fun getReportDir(): File {
return File(appDir, "reportDir")
package org.briarproject.briar.headless
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.counted
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import org.briarproject.bramble.BrambleCoreModule
import org.briarproject.briar.BriarCoreModule
import org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY
import org.spongycastle.util.encoders.Base64.toBase64String
import java.lang.System.getProperty
import java.lang.System.setProperty
import java.nio.file.Files.setPosixFilePermissions
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermission.*
import java.util.logging.Level.*
import java.util.logging.LogManager
private const val DEFAULT_PORT = 7000
private val DEFAULT_DATA_DIR = getProperty("user.home") + separator + ".briar"
private class Main : CliktCommand(
name = "briar-headless",
help = "A Briar peer without GUI that exposes a REST and Websocket API"
) {
private val debug by option("--debug", "-d", help = "Enable printing of debug messages").flag(
default = false