Merge branch 'headless' into 'master'

Add Briar headless client that exposes a REST API

See merge request briar/briar!931
This commit is contained in:
akwizgran
2018-10-09 15:43:31 +00:00
45 changed files with 2096 additions and 1 deletions

View File

@@ -36,6 +36,9 @@
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
@@ -257,5 +260,11 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
<option name="ENUM_CONSTANTS_WRAP" value="1" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -0,0 +1,23 @@
<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" />
</option>
<envs />
<patterns />
<method />
</configuration>
</component>

View 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" />
</method>
</configuration>
</component>
</component>

View File

@@ -0,0 +1,17 @@
<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 />
<method>
<option name="Gradle.BeforeRunTask" enabled="true" tasks="jar" externalProjectPath="$PROJECT_DIR$/briar-headless" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>

197
briar-headless/README.md Normal file
View File

@@ -0,0 +1,197 @@
# 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
Password:
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
DZbfoUie8sjap7CSDR9y6cgJCojV+xUITTIFbgtAgqk=
You can test that things work as expected by running:
$ curl -H "Authorization: Bearer DZbfoUie8sjap7CSDR9y6cgJCojV+xUITTIFbgtAgqk=" http://127.0.0.1:7000/v1/contacts
[]
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.
## REST API
### Listing all contacts
`GET /v1/contacts`
Returns a JSON array of contacts:
```json
{
"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:
```json
{
"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:
```json
{
"text": "Hello World!"
}
```
### Listing blog posts
`GET /v1/blogs/posts`
Returns a JSON array of blog posts:
```json
{
"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:
```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" \
http://DZbfoUie8sjap7CSDR9y6cgJCojV+xUITTIFbgtAgqk=@127.0.0.1:7000/v1/ws
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:
```json
{
"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.

View File

@@ -0,0 +1,60 @@
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 'com.google.dagger:dagger-compiler:2.0.2'
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 {
attributes(
'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 {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}

View File

@@ -0,0 +1,13 @@
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()

View File

@@ -0,0 +1,27 @@
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 java.security.SecureRandom
import javax.inject.Singleton
@Component(
modules = [
BrambleCoreModule::class,
BriarCoreModule::class,
DesktopSecureRandomModule::class,
AccountModule::class,
HeadlessModule::class
]
)
@Singleton
internal interface BriarHeadlessApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons {
fun getRouter(): Router
fun getSecureRandom(): SecureRandom
}

View File

@@ -0,0 +1,64 @@
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
@Immutable
@Singleton
internal class BriarService
@Inject
constructor(
private val accountManager: AccountManager,
private val lifecycleManager: LifecycleManager,
private val passwordStrengthEstimator: PasswordStrengthEstimator
) {
fun start() {
if (!accountManager.accountExists()) {
createAccount()
} else {
val password = prompt("Password", hideInput = true)
?: throw UsageError("Could not get password. Is STDIN connected?")
if (!accountManager.signIn(password)) {
echo("Error: Password invalid")
exit(1)
}
}
val dbKey = accountManager.databaseKey ?: throw AssertionError()
lifecycleManager.startServices(dbKey)
}
fun stop() {
lifecycleManager.stopServices()
lifecycleManager.waitForShutdown()
}
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!")
nickname
}
val password =
prompt("Password", hideInput = true, requireConfirmation = true) { password ->
if (passwordStrengthEstimator.estimateStrength(password) < QUITE_WEAK)
throw UsageError("Please enter a stronger password!")
password
}
if (nickname == null || password == null)
throw UsageError("Could not get account information. Is STDIN connected?")
accountManager.createAccount(nickname, password)
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.briar.headless
import org.briarproject.bramble.api.db.DatabaseConfig
import java.io.File
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
}
}

View File

@@ -0,0 +1,116 @@
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.network.NetworkManager
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.network.JavaNetworkModule
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.contact.HeadlessContactModule
import org.briarproject.briar.headless.event.HeadlessEventModule
import org.briarproject.briar.headless.forums.HeadlessForumModule
import org.briarproject.briar.headless.messaging.HeadlessMessagingModule
import java.io.File
import java.security.GeneralSecurityException
import java.util.Collections.emptyList
import java.util.concurrent.Executor
import javax.inject.Singleton
import javax.net.SocketFactory
@Module(
includes = [
JavaNetworkModule::class,
JavaSystemModule::class,
CircumventionModule::class,
HeadlessBlogModule::class,
HeadlessContactModule::class,
HeadlessEventModule::class,
HeadlessForumModule::class,
HeadlessMessagingModule::class
]
)
internal class HeadlessModule(private val appDir: File) {
@Provides
@Singleton
internal fun provideDatabaseConfig(): DatabaseConfig {
val dbDir = File(appDir, "db")
val keyDir = File(appDir, "key")
return HeadlessDatabaseConfig(dbDir, keyDir)
}
@Provides
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(
ioExecutor,
networkManager, locationUtils, eventBus, torSocketFactory,
backoffFactory, resourceProvider, circumventionProvider, clock,
torDirectory
)
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
}
}
}
@Provides
@Singleton
internal fun provideDevConfig(crypto: CryptoComponent): DevConfig {
return object : DevConfig {
override fun getDevPublicKey(): PublicKey {
try {
return crypto.messageKeyParser
.parsePublicKey(fromHexString(DEV_PUBLIC_KEY_HEX))
} catch (e: GeneralSecurityException) {
throw RuntimeException(e)
}
}
override fun getDevOnionAddress(): String {
return DEV_ONION_ADDRESS
}
override fun getReportDir(): File {
return File(appDir, "reportDir")
}
}
}
}

View File

@@ -0,0 +1,115 @@
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 com.github.ajalt.clikt.parameters.types.int
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.io.File
import java.io.File.separator
import java.io.IOException
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.security.SecureRandom
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
)
private val verbosity by option(
"--verbose",
"-v",
help = "Print verbose log messages"
).counted()
private val port by option(
"--port",
help = "Bind the server to this port. Default: $DEFAULT_PORT",
metavar = "PORT",
envvar = "BRIAR_PORT"
).int().default(DEFAULT_PORT)
private val dataDir by option(
"--data-dir",
help = "The directory where Briar will store its files. Default: $DEFAULT_DATA_DIR",
metavar = "PATH",
envvar = "BRIAR_DATA_DIR"
).default(DEFAULT_DATA_DIR)
override fun run() {
// logging
val levelSlf4j = if (debug) "DEBUG" else when (verbosity) {
0 -> "WARN"
1 -> "INFO"
else -> "DEBUG"
}
val level = if (debug) ALL else when (verbosity) {
0 -> WARNING
1 -> INFO
else -> ALL
}
setProperty(DEFAULT_LOG_LEVEL_KEY, levelSlf4j)
LogManager.getLogManager().getLogger("").level = level
val dataDir = getDataDir()
val app =
DaggerBriarHeadlessApp.builder().headlessModule(HeadlessModule(dataDir)).build()
// We need to load the eager singletons directly after making the
// dependency graphs
BrambleCoreModule.initEagerSingletons(app)
BriarCoreModule.initEagerSingletons(app)
val authToken = getOrCreateAuthToken(dataDir, app.getSecureRandom())
app.getRouter().start(authToken, port, debug)
}
private fun getDataDir(): File {
val file = File(dataDir)
if (!file.exists() && !file.mkdirs()) {
throw IOException("Could not create directory: ${file.absolutePath}")
} else if (!file.isDirectory) {
throw IOException("Data dir is not a directory: ${file.absolutePath}")
}
val perms = HashSet<PosixFilePermission>()
perms.add(OWNER_READ)
perms.add(OWNER_WRITE)
perms.add(OWNER_EXECUTE)
setPosixFilePermissions(file.toPath(), perms)
return file
}
private fun getOrCreateAuthToken(dataDir: File, secureRandom: SecureRandom): String {
val tokenFile = File(dataDir, "auth_token")
return if (tokenFile.isFile) {
tokenFile.readText()
} else {
val authToken = createAuthToken(secureRandom)
tokenFile.writeText(authToken)
authToken
}
}
private fun createAuthToken(secureRandom: SecureRandom): String {
val bytes = ByteArray(32)
secureRandom.nextBytes(bytes)
return toBase64String(bytes)
}
}
fun main(args: Array<String>) = Main().main(args)

View File

@@ -0,0 +1,129 @@
package org.briarproject.briar.headless
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.ObjectMapper
import io.javalin.BadRequestResponse
import io.javalin.Context
import io.javalin.Javalin
import io.javalin.JavalinEvent.SERVER_START_FAILED
import io.javalin.JavalinEvent.SERVER_STOPPED
import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.core.util.ContextUtil
import io.javalin.core.util.Header.AUTHORIZATION
import org.briarproject.briar.headless.blogs.BlogController
import org.briarproject.briar.headless.contact.ContactController
import org.briarproject.briar.headless.event.WebSocketController
import org.briarproject.briar.headless.forums.ForumController
import org.briarproject.briar.headless.messaging.MessagingController
import java.lang.Runtime.getRuntime
import java.lang.System.exit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Logger.getLogger
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
@Immutable
@Singleton
internal class Router
@Inject
constructor(
private val briarService: BriarService,
private val webSocketController: WebSocketController,
private val contactController: ContactController,
private val messagingController: MessagingController,
private val forumController: ForumController,
private val blogController: BlogController
) {
private val logger = getLogger(Router::javaClass.name)
private val stopped = AtomicBoolean(false)
fun start(authToken: String, port: Int, debug: Boolean) {
briarService.start()
getRuntime().addShutdownHook(Thread(this::stop))
val app = Javalin.create()
.port(port)
.disableStartupBanner()
.enableCaseSensitiveUrls()
.event(SERVER_START_FAILED) {serverStopped() }
.event(SERVER_STOPPED) { serverStopped() }
if (debug) app.enableDebugLogging()
app.start()
app.accessManager { handler, ctx, _ ->
if (ctx.header(AUTHORIZATION) == "Bearer $authToken") {
handler.handle(ctx)
} else {
ctx.status(401).result("Unauthorized")
}
}
app.routes {
path("/v1") {
path("/contacts") {
get { ctx -> contactController.list(ctx) }
}
path("/messages/:contactId") {
get { ctx -> messagingController.list(ctx) }
post { ctx -> messagingController.write(ctx) }
}
path("/forums") {
get { ctx -> forumController.list(ctx) }
post { ctx -> forumController.create(ctx) }
}
path("/blogs") {
path("/posts") {
get { ctx -> blogController.listPosts(ctx) }
post { ctx -> blogController.createPost(ctx) }
}
}
}
}
app.ws("/v1/ws") { ws ->
ws.onConnect { session ->
val authHeader = session.header(AUTHORIZATION)
val token = ContextUtil.getBasicAuthCredentials(authHeader)?.username
if (authToken == token) {
logger.info("Adding websocket session with ${session.remoteAddress}")
webSocketController.sessions.add(session)
} else {
logger.info("Closing websocket connection with ${session.remoteAddress}")
session.close(1008, "Invalid Authentication Token")
}
}
ws.onClose { session, _, _ ->
logger.info("Removing websocket connection with ${session.remoteAddress}")
webSocketController.sessions.remove(session)
}
}
}
private fun serverStopped() {
stop()
exit(1)
}
private fun stop() {
if (!stopped.getAndSet(true)) {
briarService.stop()
}
}
}
/**
* Returns a String from the JSON field or throws [BadRequestResponse] if null or empty.
*/
fun Context.getFromJson(field: String) : String {
try {
// TODO use a static object mapper to avoid re-initializations
val jsonNode = ObjectMapper().readTree(body())
if (!jsonNode.hasNonNull(field)) throw BadRequestResponse("'$field' missing in JSON")
val result = jsonNode.get(field).asText()
if (result == null || result.isEmpty()) throw BadRequestResponse("'$field' empty in JSON")
return result
} catch (e: JsonParseException) {
throw BadRequestResponse("Invalid JSON")
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.briar.headless.blogs
import io.javalin.Context
interface BlogController {
fun listPosts(ctx: Context): Context
fun createPost(ctx: Context): Context
}

View File

@@ -0,0 +1,51 @@
package org.briarproject.briar.headless.blogs
import io.javalin.BadRequestResponse
import io.javalin.Context
import org.briarproject.bramble.api.identity.IdentityManager
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.util.StringUtils
import org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_BODY_LENGTH
import org.briarproject.briar.api.blog.BlogManager
import org.briarproject.briar.api.blog.BlogPostFactory
import org.briarproject.briar.headless.getFromJson
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
@Immutable
@Singleton
internal class BlogControllerImpl
@Inject
constructor(
private val blogManager: BlogManager,
private val blogPostFactory: BlogPostFactory,
private val identityManager: IdentityManager,
private val clock: Clock
) : BlogController {
override fun listPosts(ctx: Context): Context {
val posts = blogManager.blogs
.flatMap { blog -> blogManager.getPostHeaders(blog.id) }
.asSequence()
.sortedBy { it.timeReceived }
.map { header -> header.output(blogManager.getPostBody(header.id)) }
.toList()
return ctx.json(posts)
}
override fun createPost(ctx: Context): Context {
val text = ctx.getFromJson("text")
if (StringUtils.utf8IsTooLong(text, MAX_BLOG_POST_BODY_LENGTH))
throw BadRequestResponse("Too long blog post text")
val author = identityManager.localAuthor
val blog = blogManager.getPersonalBlog(author)
val now = clock.currentTimeMillis()
val post = blogPostFactory.createBlogPost(blog.id, now, null, author, text)
blogManager.addLocalPost(post)
val header = blogManager.getPostHeader(blog.id, post.message.id)
return ctx.json(header.output(text))
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.headless.blogs
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class HeadlessBlogModule {
@Provides
@Singleton
internal fun provideBlogController(blogController: BlogControllerImpl): BlogController {
return blogController
}
}

View File

@@ -0,0 +1,21 @@
package org.briarproject.briar.headless.blogs
import org.briarproject.bramble.identity.output
import org.briarproject.briar.api.blog.BlogPostHeader
import org.briarproject.briar.api.blog.MessageType
import org.briarproject.briar.headless.json.JsonDict
internal fun BlogPostHeader.output(body: String) = JsonDict(
"text" to body,
"author" to author.output(),
"authorStatus" to authorStatus.output(),
"type" to type.output(),
"id" to id.bytes,
"parentId" to parentId?.bytes,
"read" to isRead,
"rssFeed" to isRssFeed,
"timestamp" to timestamp,
"timestampReceived" to timeReceived
)
internal fun MessageType.output() = name.toLowerCase()

View File

@@ -0,0 +1,9 @@
package org.briarproject.briar.headless.contact
import io.javalin.Context
interface ContactController {
fun list(ctx: Context): Context
}

View File

@@ -0,0 +1,22 @@
package org.briarproject.briar.headless.contact
import io.javalin.Context
import org.briarproject.bramble.api.contact.ContactManager
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
@Immutable
@Singleton
internal class ContactControllerImpl
@Inject
constructor(private val contactManager: ContactManager) : ContactController {
override fun list(ctx: Context): Context {
val contacts = contactManager.activeContacts.map { contact ->
contact.output()
}
return ctx.json(contacts)
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.headless.contact
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class HeadlessContactModule {
@Provides
@Singleton
internal fun provideContactController(contactController: ContactControllerImpl): ContactController {
return contactController
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.briar.headless.contact
import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.identity.output
import org.briarproject.briar.headless.json.JsonDict
internal fun Contact.output() = JsonDict(
"contactId" to id.int,
"author" to author.output(),
"verified" to isVerified
)

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.headless.event
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class HeadlessEventModule {
@Provides
@Singleton
internal fun provideWebSocketController(webSocketController: WebSocketControllerImpl): WebSocketController {
return webSocketController
}
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.briar.headless.event
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent
import org.briarproject.briar.headless.messaging.output
import javax.annotation.concurrent.Immutable
@Immutable
internal class OutputEvent(val name: String, val data: Any) {
val type = "event"
}
internal fun PrivateMessageReceivedEvent<*>.output(body: String) =
messageHeader.output(contactId, body)

View File

@@ -0,0 +1,18 @@
package org.briarproject.briar.headless.event
import io.javalin.websocket.WsSession
import org.briarproject.bramble.api.lifecycle.IoExecutor
import org.briarproject.briar.headless.json.JsonDict
import javax.annotation.concurrent.ThreadSafe
@ThreadSafe
interface WebSocketController {
val sessions: MutableSet<WsSession>
/**
* Sends an event to all open sessions using the [IoExecutor].
*/
fun sendEvent(name: String, obj: JsonDict)
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.briar.headless.event
import io.javalin.json.JavalinJson.toJson
import io.javalin.websocket.WsSession
import org.briarproject.bramble.api.lifecycle.IoExecutor
import org.briarproject.bramble.util.LogUtils.logException
import org.briarproject.briar.headless.json.JsonDict
import org.eclipse.jetty.websocket.api.WebSocketException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.logging.Level.WARNING
import java.util.logging.Logger.getLogger
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
@Immutable
@Singleton
internal class WebSocketControllerImpl
@Inject
constructor(@IoExecutor private val ioExecutor: Executor) : WebSocketController {
private val logger = getLogger(WebSocketControllerImpl::javaClass.name)
override val sessions: MutableSet<WsSession> = ConcurrentHashMap.newKeySet<WsSession>()
override fun sendEvent(name: String, obj: JsonDict) {
val event = toJson(OutputEvent(name, obj))
sessions.forEach { session ->
ioExecutor.execute {
try {
session.send(event)
} catch (e: WebSocketException) {
logException(logger, WARNING, e)
} catch (e: IOException) {
logException(logger, WARNING, e)
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.briar.headless.forums
import io.javalin.Context
interface ForumController {
fun list(ctx: Context): Context
fun create(ctx: Context): Context
}

View File

@@ -0,0 +1,30 @@
package org.briarproject.briar.headless.forums
import io.javalin.BadRequestResponse
import io.javalin.Context
import org.briarproject.bramble.util.StringUtils
import org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH
import org.briarproject.briar.api.forum.ForumManager
import org.briarproject.briar.headless.getFromJson
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
@Immutable
@Singleton
internal class ForumControllerImpl
@Inject
constructor(private val forumManager: ForumManager) : ForumController {
override fun list(ctx: Context): Context {
return ctx.json(forumManager.forums.output())
}
override fun create(ctx: Context): Context {
val name = ctx.getFromJson("name")
if (StringUtils.utf8IsTooLong(name, MAX_FORUM_NAME_LENGTH))
throw BadRequestResponse("Forum name is too long")
return ctx.json(forumManager.addForum(name).output())
}
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.headless.forums
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class HeadlessForumModule {
@Provides
@Singleton
internal fun provideForumController(forumController: ForumControllerImpl): ForumController {
return forumController
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.briar.headless.forums
import org.briarproject.briar.api.forum.Forum
import org.briarproject.briar.headless.json.JsonDict
internal fun Forum.output() = JsonDict(
"name" to name,
"id" to id.bytes
)
internal fun Collection<Forum>.output() = map { it.output() }

View File

@@ -0,0 +1,11 @@
package org.briarproject.briar.headless.json
class JsonDict(vararg pairs: Pair<String, Any?>) : HashMap<String, Any?>(pairs.size) {
init {
putAll(pairs)
}
fun putAll(vararg pairs: Pair<String, Any?>) {
for (p in pairs) put(p.first, p.second)
}
}

View File

@@ -0,0 +1,20 @@
package org.briarproject.briar.headless.messaging
import dagger.Module
import dagger.Provides
import org.briarproject.bramble.api.event.EventBus
import javax.inject.Singleton
@Module
class HeadlessMessagingModule {
@Provides
@Singleton
internal fun provideMessagingController(
eventBus: EventBus, messagingController: MessagingControllerImpl
): MessagingController {
eventBus.addListener(messagingController)
return messagingController
}
}

View File

@@ -0,0 +1,11 @@
package org.briarproject.briar.headless.messaging
import io.javalin.Context
interface MessagingController {
fun list(ctx: Context): Context
fun write(ctx: Context): Context
}

View File

@@ -0,0 +1,123 @@
package org.briarproject.briar.headless.messaging
import io.javalin.BadRequestResponse
import io.javalin.Context
import io.javalin.NotFoundResponse
import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.api.contact.ContactManager
import org.briarproject.bramble.api.db.DatabaseExecutor
import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.api.event.Event
import org.briarproject.bramble.api.event.EventListener
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.util.StringUtils.utf8IsTooLong
import org.briarproject.briar.api.blog.BlogInvitationRequest
import org.briarproject.briar.api.blog.BlogInvitationResponse
import org.briarproject.briar.api.forum.ForumInvitationRequest
import org.briarproject.briar.api.forum.ForumInvitationResponse
import org.briarproject.briar.api.introduction.IntroductionRequest
import org.briarproject.briar.api.introduction.IntroductionResponse
import org.briarproject.briar.api.messaging.*
import org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse
import org.briarproject.briar.headless.event.WebSocketController
import org.briarproject.briar.headless.event.output
import org.briarproject.briar.headless.getFromJson
import org.briarproject.briar.headless.json.JsonDict
import java.util.concurrent.Executor
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton
internal const val EVENT_PRIVATE_MESSAGE = "PrivateMessageReceivedEvent"
@Immutable
@Singleton
internal class MessagingControllerImpl
@Inject
constructor(
private val messagingManager: MessagingManager,
private val conversationManager: ConversationManager,
private val privateMessageFactory: PrivateMessageFactory,
private val contactManager: ContactManager,
private val webSocketController: WebSocketController,
@DatabaseExecutor private val dbExecutor: Executor,
private val clock: Clock
) : MessagingController, EventListener {
override fun list(ctx: Context): Context {
val contact = getContact(ctx)
val jsonVisitor = JsonVisitor(contact.id, messagingManager)
val messages = conversationManager.getMessageHeaders(contact.id)
.sortedBy { it.timestamp }
.map { header -> header.accept(jsonVisitor) }
return ctx.json(messages)
}
override fun write(ctx: Context): Context {
val contact = getContact(ctx)
val message = ctx.getFromJson("text")
if (utf8IsTooLong(message, MAX_PRIVATE_MESSAGE_BODY_LENGTH))
throw BadRequestResponse("Message text too large")
val group = messagingManager.getContactGroup(contact)
val now = clock.currentTimeMillis()
val m = privateMessageFactory.createPrivateMessage(group.id, now, message)
messagingManager.addLocalMessage(m)
return ctx.json(m.output(contact.id, message))
}
override fun eventOccurred(e: Event) {
when (e) {
is PrivateMessageReceivedEvent<*> -> dbExecutor.execute {
val body = messagingManager.getMessageBody(e.messageHeader.id)
webSocketController.sendEvent(EVENT_PRIVATE_MESSAGE, e.output(body))
}
}
}
private fun getContact(ctx: Context): Contact {
val contactString = ctx.pathParam("contactId")
val contactInt = try {
Integer.parseInt(contactString)
} catch (e: NumberFormatException) {
throw NotFoundResponse()
}
val contactId = ContactId(contactInt)
return try {
contactManager.getContact(contactId)
} catch (e: NoSuchContactException) {
throw NotFoundResponse()
}
}
}
private class JsonVisitor(
private val contactId: ContactId,
private val messagingManager: MessagingManager
) : PrivateMessageVisitor<JsonDict> {
override fun visitPrivateMessageHeader(h: PrivateMessageHeader) =
h.output(contactId, messagingManager.getMessageBody(h.id))
override fun visitBlogInvitationRequest(r: BlogInvitationRequest) = r.output(contactId)
override fun visitBlogInvitationResponse(r: BlogInvitationResponse) = r.output(contactId)
override fun visitForumInvitationRequest(r: ForumInvitationRequest) = r.output(contactId)
override fun visitForumInvitationResponse(r: ForumInvitationResponse) = r.output(contactId)
override fun visitGroupInvitationRequest(r: GroupInvitationRequest) = r.output(contactId)
override fun visitGroupInvitationResponse(r: GroupInvitationResponse) = r.output(contactId)
override fun visitIntroductionRequest(r: IntroductionRequest) = r.output(contactId)
override fun visitIntroductionResponse(r: IntroductionResponse) = r.output(contactId)
}

View File

@@ -0,0 +1,40 @@
package org.briarproject.briar.headless.messaging
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.briar.api.messaging.PrivateMessage
import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.headless.json.JsonDict
internal fun PrivateMessageHeader.output(contactId: ContactId) = JsonDict(
"type" to "PrivateMessage",
"contactId" to contactId.int,
"timestamp" to timestamp,
"read" to isRead,
"seen" to isSeen,
"sent" to isSent,
"local" to isLocal,
"id" to id.bytes,
"groupId" to groupId.bytes
)
internal fun PrivateMessageHeader.output(contactId: ContactId, body: String?): JsonDict {
val dict = output(contactId)
dict["text"] = body
return dict
}
/**
* Use only for outgoing messages that were just sent
*/
internal fun PrivateMessage.output(contactId: ContactId, body: String) = JsonDict(
"type" to "PrivateMessage",
"contactId" to contactId.int,
"timestamp" to message.timestamp,
"read" to true,
"seen" to false,
"sent" to false,
"local" to true,
"id" to message.id.bytes,
"groupId" to message.groupId.bytes,
"text" to body
)

View File

@@ -0,0 +1,54 @@
package org.briarproject.briar.headless.messaging
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.briar.api.blog.BlogInvitationRequest
import org.briarproject.briar.api.forum.ForumInvitationRequest
import org.briarproject.briar.api.introduction.IntroductionRequest
import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.api.messaging.PrivateRequest
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest
import org.briarproject.briar.api.sharing.InvitationRequest
import org.briarproject.briar.headless.json.JsonDict
internal fun PrivateRequest<*>.output(contactId: ContactId): JsonDict {
val dict = (this as PrivateMessageHeader).output(contactId, message)
dict.putAll(
"sessionId" to sessionId.bytes,
"name" to name,
"answered" to wasAnswered()
)
return dict
}
internal fun IntroductionRequest.output(contactId: ContactId): JsonDict {
val dict = (this as PrivateRequest<*>).output(contactId)
dict.putAll(
"type" to "IntroductionRequest",
"alreadyContact" to isContact
)
return dict
}
internal fun InvitationRequest<*>.output(contactId: ContactId): JsonDict {
val dict = (this as PrivateRequest<*>).output(contactId)
dict["canBeOpened"] = canBeOpened()
return dict
}
internal fun BlogInvitationRequest.output(contactId: ContactId): JsonDict {
val dict = (this as InvitationRequest<*>).output(contactId)
dict["type"] = "BlogInvitationRequest"
return dict
}
internal fun ForumInvitationRequest.output(contactId: ContactId): JsonDict {
val dict = (this as InvitationRequest<*>).output(contactId)
dict["type"] = "ForumInvitationRequest"
return dict
}
internal fun GroupInvitationRequest.output(contactId: ContactId): JsonDict {
val dict = (this as InvitationRequest<*>).output(contactId)
dict["type"] = "GroupInvitationRequest"
return dict
}

View File

@@ -0,0 +1,55 @@
package org.briarproject.briar.headless.messaging
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.identity.output
import org.briarproject.briar.api.blog.BlogInvitationResponse
import org.briarproject.briar.api.forum.ForumInvitationResponse
import org.briarproject.briar.api.introduction.IntroductionResponse
import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.api.messaging.PrivateResponse
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse
import org.briarproject.briar.api.sharing.InvitationResponse
import org.briarproject.briar.headless.json.JsonDict
internal fun PrivateResponse.output(contactId: ContactId): JsonDict {
val dict = (this as PrivateMessageHeader).output(contactId)
dict.putAll(
"sessionId" to sessionId.bytes,
"accepted" to wasAccepted()
)
return dict
}
internal fun IntroductionResponse.output(contactId: ContactId): JsonDict {
val dict = (this as PrivateResponse).output(contactId)
dict.putAll(
"type" to "IntroductionResponse",
"introducedAuthor" to introducedAuthor.output(),
"introducer" to isIntroducer
)
return dict
}
internal fun InvitationResponse.output(contactId: ContactId): JsonDict {
val dict = (this as PrivateResponse).output(contactId)
dict["shareableId"] = shareableId.bytes
return dict
}
internal fun BlogInvitationResponse.output(contactId: ContactId): JsonDict {
val dict = (this as InvitationResponse).output(contactId)
dict["type"] = "BlogInvitationResponse"
return dict
}
internal fun ForumInvitationResponse.output(contactId: ContactId): JsonDict {
val dict = (this as InvitationResponse).output(contactId)
dict["type"] = "ForumInvitationResponse"
return dict
}
internal fun GroupInvitationResponse.output(contactId: ContactId): JsonDict {
val dict = (this as InvitationResponse).output(contactId)
dict["type"] = "GroupInvitationResponse"
return dict
}

View File

@@ -0,0 +1,45 @@
package org.briarproject.briar.headless
import io.javalin.Context
import io.javalin.core.util.ContextUtil
import io.mockk.mockk
import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.api.contact.ContactManager
import org.briarproject.bramble.api.identity.Author
import org.briarproject.bramble.api.identity.IdentityManager
import org.briarproject.bramble.api.identity.LocalAuthor
import org.briarproject.bramble.api.sync.Group
import org.briarproject.bramble.api.sync.Message
import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.test.TestUtils.*
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.skyscreamer.jsonassert.JSONAssert.assertEquals
import org.skyscreamer.jsonassert.JSONCompareMode.STRICT
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
abstract class ControllerTest {
protected val contactManager = mockk<ContactManager>()
protected val identityManager = mockk<IdentityManager>()
protected val clock = mockk<Clock>()
protected val ctx = mockk<Context>()
private val request = mockk<HttpServletRequest>(relaxed = true)
private val response = mockk<HttpServletResponse>(relaxed = true)
private val outputCtx = ContextUtil.init(request, response)
protected val group: Group = getGroup(getClientId(), 0)
protected val author: Author = getAuthor()
protected val localAuthor: LocalAuthor = getLocalAuthor()
protected val contact = Contact(ContactId(1), author, localAuthor.id, true, true)
protected val message: Message = getMessage(group.id)
protected val body: String = getRandomString(5)
protected val timestamp = 42L
protected fun assertJsonEquals(json: String, obj: Any) {
assertEquals(json, outputCtx.json(obj).resultString(), STRICT)
}
}

View File

@@ -0,0 +1,120 @@
package org.briarproject.briar.headless.blogs
import io.javalin.BadRequestResponse
import io.javalin.json.JavalinJson.toJson
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.briarproject.bramble.api.identity.Author.Status.OURSELVES
import org.briarproject.bramble.api.sync.MessageId
import org.briarproject.bramble.identity.output
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.api.blog.*
import org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_BODY_LENGTH
import org.briarproject.briar.api.blog.MessageType.POST
import org.briarproject.briar.headless.ControllerTest
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
internal class BlogControllerTest : ControllerTest() {
private val blogManager = mockk<BlogManager>()
private val blogPostFactory = mockk<BlogPostFactory>()
private val controller =
BlogControllerImpl(blogManager, blogPostFactory, identityManager, clock)
private val blog = Blog(group, author, false)
private val parentId: MessageId? = null
private val rssFeed = false
private val read = true
private val header = BlogPostHeader(
POST, group.id, message.id, parentId, message.timestamp, timestamp, author, OURSELVES,
rssFeed, read
)
@Test
fun testCreate() {
val post = BlogPost(message, null, localAuthor)
every { ctx.body() } returns """{"text": "$body"}"""
every { identityManager.localAuthor } returns localAuthor
every { blogManager.getPersonalBlog(localAuthor) } returns blog
every { clock.currentTimeMillis() } returns message.timestamp
every {
blogPostFactory.createBlogPost(
message.groupId,
message.timestamp,
parentId,
localAuthor,
body
)
} returns post
every { blogManager.addLocalPost(post) } just Runs
every { blogManager.getPostHeader(post.message.groupId, post.message.id) } returns header
every { ctx.json(header.output(body)) } returns ctx
controller.createPost(ctx)
}
@Test
fun testCreateNoText() {
every { ctx.body() } returns """{"foo": "bar"}"""
assertThrows(BadRequestResponse::class.java) { controller.createPost(ctx) }
}
@Test
fun testCreateEmptyText() {
every { ctx.body() } returns """{"text": ""}"""
assertThrows(BadRequestResponse::class.java) { controller.createPost(ctx) }
}
@Test
fun testCreateTooLongText() {
every { ctx.body() } returns """{"text": "${getRandomString(MAX_BLOG_POST_BODY_LENGTH + 1)}"}"""
assertThrows(BadRequestResponse::class.java) { controller.createPost(ctx) }
}
@Test
fun testList() {
every { blogManager.blogs } returns listOf(blog)
every { blogManager.getPostHeaders(group.id) } returns listOf(header)
every { blogManager.getPostBody(message.id) } returns body
every { ctx.json(listOf(header.output(body))) } returns ctx
controller.listPosts(ctx)
}
@Test
fun testEmptyList() {
every { blogManager.blogs } returns listOf(blog)
every { blogManager.getPostHeaders(group.id) } returns emptyList()
every { ctx.json(emptyList<Any>()) } returns ctx
controller.listPosts(ctx)
}
@Test
fun testOutputBlogPost() {
val json = """
{
"text": "$body",
"author": ${toJson(author.output())},
"authorStatus": "ourselves",
"type": "post",
"id": ${toJson(header.id.bytes)},
"parentId": $parentId,
"read": $read,
"rssFeed": $rssFeed,
"timestamp": ${message.timestamp},
"timestampReceived": $timestamp
}
"""
assertJsonEquals(json, header.output(body))
}
}

View File

@@ -0,0 +1,53 @@
package org.briarproject.briar.headless.contact
import io.javalin.json.JavalinJson.toJson
import io.mockk.every
import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.identity.output
import org.briarproject.briar.headless.ControllerTest
import org.junit.jupiter.api.Test
internal class ContactControllerTest : ControllerTest() {
private val controller = ContactControllerImpl(contactManager)
@Test
fun testEmptyContactList() {
every { contactManager.activeContacts } returns emptyList<Contact>()
every { ctx.json(emptyList<Any>()) } returns ctx
controller.list(ctx)
}
@Test
fun testList() {
every { contactManager.activeContacts } returns listOf(contact)
every { ctx.json(listOf(contact.output())) } returns ctx
controller.list(ctx)
}
@Test
fun testOutputContact() {
val json = """
{
"contactId": ${contact.id.int},
"author": ${toJson(author.output())},
"verified": ${contact.isVerified}
}
"""
assertJsonEquals(json, contact.output())
}
@Test
fun testOutputAuthor() {
val json = """
{
"formatVersion": 1,
"id": ${toJson(author.id.bytes)},
"name": "${author.name}",
"publicKey": ${toJson(author.publicKey)}
}
"""
assertJsonEquals(json, author.output())
}
}

View File

@@ -0,0 +1,75 @@
package org.briarproject.briar.headless.event
import io.javalin.json.JavalinJson.toJson
import io.javalin.websocket.WsSession
import io.mockk.*
import org.briarproject.bramble.test.ImmediateExecutor
import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent
import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.messaging.EVENT_PRIVATE_MESSAGE
import org.briarproject.briar.headless.messaging.output
import org.eclipse.jetty.websocket.api.WebSocketException
import org.junit.jupiter.api.Test
import java.io.IOException
internal class WebSocketControllerTest : ControllerTest() {
private val session1 = mockk<WsSession>()
private val session2 = mockk<WsSession>()
private val controller = WebSocketControllerImpl(ImmediateExecutor())
private val header =
PrivateMessageHeader(message.id, group.id, timestamp, true, true, true, true)
private val event = PrivateMessageReceivedEvent(header, contact.id)
private val outputEvent = OutputEvent(EVENT_PRIVATE_MESSAGE, event.output(body))
@Test
fun testSendEvent() {
val slot = CapturingSlot<String>()
every { session1.send(capture(slot)) } just Runs
controller.sessions.add(session1)
controller.sendEvent(EVENT_PRIVATE_MESSAGE, event.output(body))
assertJsonEquals(slot.captured, outputEvent)
}
@Test
fun testSendEventIOException() {
testSendEventException(IOException())
}
@Test
fun testSendEventWebSocketException() {
testSendEventException(WebSocketException())
}
private fun testSendEventException(throwable: Throwable) {
val slot = CapturingSlot<String>()
every { session1.send(capture(slot)) } throws throwable
every { session2.send(capture(slot)) } just Runs
controller.sessions.add(session1)
controller.sessions.add(session2)
controller.sendEvent(EVENT_PRIVATE_MESSAGE, event.output(body))
verify { session2.send(slot.captured) }
}
@Test
fun testOutputPrivateMessageReceivedEvent() {
val json = """
{
"type": "event",
"name": "PrivateMessageReceivedEvent",
"data": ${toJson(header.output(contact.id, body))}
}
"""
assertJsonEquals(json, outputEvent)
}
}

View File

@@ -0,0 +1,75 @@
package org.briarproject.briar.headless.forums
import io.javalin.BadRequestResponse
import io.mockk.every
import io.mockk.mockk
import org.briarproject.bramble.test.TestUtils.getRandomBytes
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.api.forum.Forum
import org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH
import org.briarproject.briar.api.forum.ForumManager
import org.briarproject.briar.headless.ControllerTest
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
internal class ForumControllerTest : ControllerTest() {
private val forumManager = mockk<ForumManager>()
private val controller = ForumControllerImpl(forumManager)
private val forum = Forum(group, getRandomString(5), getRandomBytes(5))
@Test
fun list() {
every { forumManager.forums } returns listOf(forum)
every { ctx.json(listOf(forum.output())) } returns ctx
controller.list(ctx)
}
@Test
fun create() {
every { ctx.body() } returns """{"name": "${forum.name}"}"""
every { forumManager.addForum(forum.name) } returns forum
every { ctx.json(forum.output()) } returns ctx
controller.create(ctx)
}
@Test
fun createNoName() {
every { ctx.body() } returns "{}"
assertThrows(BadRequestResponse::class.java) { controller.create(ctx) }
}
@Test
fun createEmptyName() {
every { ctx.body() } returns """{"name": ""}"""
assertThrows(BadRequestResponse::class.java) { controller.create(ctx) }
}
@Test
fun createNullName() {
every { ctx.body() } returns """{"name": null}"""
assertThrows(BadRequestResponse::class.java) { controller.create(ctx) }
}
@Test
fun createNoJsonName() {
every { ctx.body() } returns "foo"
assertThrows(BadRequestResponse::class.java) { controller.create(ctx) }
}
@Test
fun createTooLongName() {
every { ctx.body() } returns """{"name": "${getRandomString(MAX_FORUM_NAME_LENGTH + 1)}"}"""
assertThrows(BadRequestResponse::class.java) { controller.create(ctx) }
}
}

View File

@@ -0,0 +1,243 @@
package org.briarproject.briar.headless.messaging
import io.javalin.BadRequestResponse
import io.javalin.Context
import io.javalin.NotFoundResponse
import io.javalin.json.JavalinJson.toJson
import io.mockk.*
import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.test.ImmediateExecutor
import org.briarproject.bramble.test.TestUtils.getRandomId
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.api.client.SessionId
import org.briarproject.briar.api.introduction.IntroductionRequest
import org.briarproject.briar.api.messaging.*
import org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent
import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.event.WebSocketController
import org.briarproject.briar.headless.event.output
import org.briarproject.briar.headless.json.JsonDict
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
internal class MessagingControllerImplTest : ControllerTest() {
private val messagingManager = mockk<MessagingManager>()
private val conversationManager = mockk<ConversationManager>()
private val privateMessageFactory = mockk<PrivateMessageFactory>()
private val webSocketController = mockk<WebSocketController>()
private val dbExecutor = ImmediateExecutor()
private val controller = MessagingControllerImpl(
messagingManager,
conversationManager,
privateMessageFactory,
contactManager,
webSocketController,
dbExecutor,
clock
)
private val header =
PrivateMessageHeader(message.id, group.id, timestamp, true, true, true, true)
private val sessionId = SessionId(getRandomId())
private val privateMessage = PrivateMessage(message)
@Test
fun list() {
expectGetContact()
every { conversationManager.getMessageHeaders(contact.id) } returns listOf(header)
every { messagingManager.getMessageBody(message.id) } returns body
every { ctx.json(listOf(header.output(contact.id, body))) } returns ctx
controller.list(ctx)
}
@Test
fun listIntroductionRequest() {
val request = IntroductionRequest(
message.id, group.id, timestamp, true, true, false, true, sessionId, author, body,
false, false
)
expectGetContact()
every { conversationManager.getMessageHeaders(contact.id) } returns listOf(request)
every { ctx.json(listOf(request.output(contact.id))) } returns ctx
controller.list(ctx)
}
@Test
fun emptyList() {
every { ctx.pathParam("contactId") } returns contact.id.int.toString()
every { contactManager.getContact(contact.id) } returns contact
every { conversationManager.getMessageHeaders(contact.id) } returns emptyList<PrivateMessageHeader>()
every { ctx.json(emptyList<Any>()) } returns ctx
controller.list(ctx)
}
@Test
fun listInvalidContactId() {
testInvalidContactId { controller.list(ctx) }
}
@Test
fun listNonexistentContactId() {
testNonexistentContactId { controller.list(ctx) }
}
@Test
fun write() {
val slot = CapturingSlot<JsonDict>()
expectGetContact()
every { ctx.body() } returns """{"text": "$body"}"""
every { messagingManager.getContactGroup(contact) } returns group
every { clock.currentTimeMillis() } returns timestamp
every {
privateMessageFactory.createPrivateMessage(
group.id,
timestamp,
body
)
} returns privateMessage
every { messagingManager.addLocalMessage(privateMessage) } just runs
every { ctx.json(capture(slot)) } returns ctx
controller.write(ctx)
assertEquals(privateMessage.output(contact.id, body), slot.captured)
}
@Test
fun writeInvalidContactId() {
testInvalidContactId { controller.write(ctx) }
}
@Test
fun writeNonexistentContactId() {
testNonexistentContactId { controller.write(ctx) }
}
@Test
fun writeNonexistentBody() {
expectGetContact()
every { ctx.body() } returns """{"foo": "bar"}"""
assertThrows(BadRequestResponse::class.java) { controller.write(ctx) }
}
@Test
fun writeEmptyBody() {
expectGetContact()
every { ctx.body() } returns """{"text": ""}"""
assertThrows(BadRequestResponse::class.java) { controller.write(ctx) }
}
@Test
fun writeTooLongBody() {
expectGetContact()
every { ctx.body() } returns """{"text": "${getRandomString(MAX_PRIVATE_MESSAGE_BODY_LENGTH + 1)}"}"""
assertThrows(BadRequestResponse::class.java) { controller.write(ctx) }
}
@Test
fun privateMessageEvent() {
val event = PrivateMessageReceivedEvent(header, contact.id)
every { messagingManager.getMessageBody(message.id) } returns body
every { webSocketController.sendEvent(EVENT_PRIVATE_MESSAGE, event.output(body)) } just runs
controller.eventOccurred(event)
}
@Test
fun testOutputPrivateMessageHeader() {
val json = """
{
"text": "$body",
"type": "PrivateMessage",
"timestamp": $timestamp,
"groupId": ${toJson(header.groupId.bytes)},
"contactId": ${contact.id.int},
"local": ${header.isLocal},
"seen": ${header.isSeen},
"read": ${header.isRead},
"sent": ${header.isSent},
"id": ${toJson(header.id.bytes)}
}
"""
assertJsonEquals(json, header.output(contact.id, body))
}
@Test
fun testOutputPrivateMessage() {
val json = """
{
"text": "$body",
"type": "PrivateMessage",
"timestamp": ${message.timestamp},
"groupId": ${toJson(message.groupId.bytes)},
"contactId": ${contact.id.int},
"local": true,
"seen": false,
"read": true,
"sent": false,
"id": ${toJson(message.id.bytes)}
}
"""
assertJsonEquals(json, privateMessage.output(contact.id, body))
}
@Test
fun testIntroductionRequestWithEmptyBody() {
val request = IntroductionRequest(
message.id, group.id, timestamp, true, true, false, true, sessionId, author, null,
false, false
)
val json = """
{
"text": null,
"type": "IntroductionRequest",
"timestamp": $timestamp,
"groupId": ${toJson(request.groupId.bytes)},
"contactId": ${contact.id.int},
"local": ${request.isLocal},
"seen": ${request.isSeen},
"read": ${request.isRead},
"sent": ${request.isSent},
"id": ${toJson(request.id.bytes)},
"sessionId": ${toJson(request.sessionId.bytes)},
"name": ${request.name},
"answered": ${request.wasAnswered()},
"alreadyContact": ${request.isContact}
}
"""
assertJsonEquals(json, request.output(contact.id))
}
private fun expectGetContact() {
every { ctx.pathParam("contactId") } returns contact.id.int.toString()
every { contactManager.getContact(contact.id) } returns contact
}
private fun testNonexistentContactId(function: () -> Context) {
every { ctx.pathParam("contactId") } returns "42"
every { contactManager.getContact(ContactId(42)) } throws NoSuchContactException()
assertThrows(NotFoundResponse::class.java) { function.invoke() }
}
private fun testInvalidContactId(function: () -> Context) {
every { ctx.pathParam("contactId") } returns "foo"
assertThrows(NotFoundResponse::class.java) { function.invoke() }
}
}

View File

@@ -0,0 +1,61 @@
dependencyVerification {
verify = [
'com.fasterxml.jackson.core:jackson-annotations:2.9.0:jackson-annotations-2.9.0.jar:45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a',
'com.fasterxml.jackson.core:jackson-core:2.9.6:jackson-core-2.9.6.jar:fab8746aedd6427788ee390ea04d438ec141bff7eb3476f8bdd5d9110fb2718a',
'com.fasterxml.jackson.core:jackson-databind:2.9.6:jackson-databind-2.9.6.jar:657e3e979446d61f88432b9c50f0ccd9c1fe4f1c822d533f5572e4c0d172a125',
'com.github.ajalt:clikt:1.5.0:clikt-1.5.0.jar:f13ab614cb0372229f6bb1e19aa98ee6f4ac96f253d0e72d482ee4f5fd2a13a9',
'com.google.dagger:dagger-compiler:2.0.2:dagger-compiler-2.0.2.jar:b74bc9de063dd4c6400b232231f2ef5056145b8fbecbf5382012007dd1c071b3',
'com.google.dagger:dagger-producers:2.0-beta:dagger-producers-2.0-beta.jar:99ec15e8a0507ba569e7655bc1165ee5e5ca5aa914b3c8f7e2c2458f724edd6b',
'com.google.dagger:dagger:2.0.2:dagger-2.0.2.jar:84c0282ed8be73a29e0475d639da030b55dee72369e58dd35ae7d4fe6243dcf9',
'com.google.guava:guava:18.0:guava-18.0.jar:d664fbfc03d2e5ce9cab2a44fb01f1d0bf9dfebeccc1a473b1f9ea31f79f6f99',
'com.vaadin.external.google:android-json:0.0.20131108.vaadin1:android-json-0.0.20131108.vaadin1.jar:dfb7bae2f404cfe0b72b4d23944698cb716b7665171812a0a4d0f5926c0fac79',
'io.javalin:javalin:2.2.0:javalin-2.2.0.jar:f7298fa281400559e92f000477a631c75aca9e01776962fd4b392fdb3b714190',
'io.mockk:mockk-agent-api:1.8.6:mockk-agent-api-1.8.6.jar:613512c66538e6349e03df641a868f4ee324f13e2e1dbd67a0ed388aa664a444',
'io.mockk:mockk-agent-common:1.8.6:mockk-agent-common-1.8.6.jar:cb7cb26fae5bfd3c89090858548990f311b27f673b9efa9d0c94f97c463b2863',
'io.mockk:mockk-agent-jvm:1.8.6:mockk-agent-jvm-1.8.6.jar:3f30b98d23ada8b5a44d75b43cd58fc03252fcb96939ff31e7ad659818af1e5d',
'io.mockk:mockk-common:1.8.6:mockk-common-1.8.6.jar:a04b0e2fc7d583807cf89f3bbf5c7501808725e49e385d95486e1008d8ab2ba8',
'io.mockk:mockk-dsl-jvm:1.8.6:mockk-dsl-jvm-1.8.6.jar:c2c5df747ff04d1a3e02212b7b43f9ba4233597ae278928598275d7a7bb26d73',
'io.mockk:mockk-dsl:1.8.6:mockk-dsl-1.8.6.jar:f6014265fe88ef1290c936741bdd0a7c3d9ceba9ee3bd2a153d65b05e1fc7946',
'io.mockk:mockk:1.8.6:mockk-1.8.6.jar:0a200d71bab11facfe50637b1980f53c07a21bfa4dd9eb021ac8e8cc693924b2',
'javax.inject:javax.inject:1:javax.inject-1.jar:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'javax.servlet:javax.servlet-api:3.1.0:javax.servlet-api-3.1.0.jar:af456b2dd41c4e82cf54f3e743bc678973d9fe35bd4d3071fa05c7e5333b8482',
'net.bytebuddy:byte-buddy-agent:1.8.8:byte-buddy-agent-1.8.8.jar:dc1a2dcefe72731fa89ae84e32231c74d545ccf8216c79865096e546f20c57e8',
'net.bytebuddy:byte-buddy:1.8.8:byte-buddy-1.8.8.jar:30aed1ae2ee5261b1d2f0e98ec3fcb40755c3f61b378089fb65d56098df1f16b',
'org.apiguardian:apiguardian-api:1.0.0:apiguardian-api-1.0.0.jar:1f58b77470d8d147a0538d515347dd322f49a83b9e884b8970051160464b65b3',
'org.eclipse.jetty.websocket:websocket-api:9.4.12.v20180830:websocket-api-9.4.12.v20180830.jar:6f7ecb42601058ffe4a6c19c5340cac3ebf0f83e2e252b457558f104238278e3',
'org.eclipse.jetty.websocket:websocket-client:9.4.12.v20180830:websocket-client-9.4.12.v20180830.jar:97c6882c858a75776773eaccc01739757c4e9f60a51613878c1f2b2ba03d91af',
'org.eclipse.jetty.websocket:websocket-common:9.4.12.v20180830:websocket-common-9.4.12.v20180830.jar:3c35aefa720c51e09532c16fdbfaaebd1af3e07dee699dacaba8e0ab0adf88e5',
'org.eclipse.jetty.websocket:websocket-server:9.4.12.v20180830:websocket-server-9.4.12.v20180830.jar:7b1bd39006be8c32d7426a119567d860b3e4a3dc3c01a5c91326450bb0213a03',
'org.eclipse.jetty.websocket:websocket-servlet:9.4.12.v20180830:websocket-servlet-9.4.12.v20180830.jar:8d43e0882759ecd093bd1a5a0ef2b4db38ac279212488a34edb8d7de7c45cc4d',
'org.eclipse.jetty:jetty-client:9.4.12.v20180830:jetty-client-9.4.12.v20180830.jar:62efbbfda88cd4f7644242c4b4df8f3b0a671bfeafea7682dabe00352ba07db7',
'org.eclipse.jetty:jetty-http:9.4.12.v20180830:jetty-http-9.4.12.v20180830.jar:20547da653be9942cc63f57e632a732608559aebde69753bc7312cfe16e8d9c0',
'org.eclipse.jetty:jetty-io:9.4.12.v20180830:jetty-io-9.4.12.v20180830.jar:ab1784abbb9e0ed0869ab6568fe46f1faa79fb5e948cf96450daecd9d27ba1db',
'org.eclipse.jetty:jetty-security:9.4.12.v20180830:jetty-security-9.4.12.v20180830.jar:513184970c785ac830424a9c62c2fadfa77a630f44aa0bdd792f00aaa092887e',
'org.eclipse.jetty:jetty-server:9.4.12.v20180830:jetty-server-9.4.12.v20180830.jar:4833644e5c5a09bbddc85f75c53e0c8ed750de120ba248fffd8508028528252d',
'org.eclipse.jetty:jetty-servlet:9.4.12.v20180830:jetty-servlet-9.4.12.v20180830.jar:7310d4cccf8abf27fde0c3f1a32e19c75fe33c6f1ab558f0704d915f0f01cb07',
'org.eclipse.jetty:jetty-util:9.4.12.v20180830:jetty-util-9.4.12.v20180830.jar:60ad53e118a3e7d10418b155b9944d90b2e4e4c732e53ef4f419473288d3f48c',
'org.eclipse.jetty:jetty-webapp:9.4.12.v20180830:jetty-webapp-9.4.12.v20180830.jar:5301e412a32bf7dddcfad458d952179597c61f8fd531c265873562725c3d4646',
'org.eclipse.jetty:jetty-xml:9.4.12.v20180830:jetty-xml-9.4.12.v20180830.jar:5b8298ab3d43ddaf0941d41f51b82c8ae23a247da055fa161b752ab9495155ed',
'org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.2.70:kotlin-annotation-processing-gradle-1.2.70.jar:820da7e3637066c14eb3d54dc29cd6d4dc4a041ff603d0b15844403de47b7d12',
'org.jetbrains.kotlin:kotlin-compiler-embeddable:1.2.70:kotlin-compiler-embeddable-1.2.70.jar:8958d6f6ce4e49a6cecaaa9a1711a6b03df793fe066a74d88cf4958f20b0f10d',
'org.jetbrains.kotlin:kotlin-reflect:1.2.41:kotlin-reflect-1.2.41.jar:1bab75771dfa2bb5949cd383ceaedf6f8d354fa0d677804fc5a39e320bab70d3',
'org.jetbrains.kotlin:kotlin-reflect:1.2.70:kotlin-reflect-1.2.70.jar:89ef46a458c5ee58b8460d0d22b0bb484eea0589ccffd59d650ef38bcb60e806',
'org.jetbrains.kotlin:kotlin-script-runtime:1.2.70:kotlin-script-runtime-1.2.70.jar:0124dfcf890e39250c3a4481cdd27f038d8321e93ce983730c4cbc10143eadc2',
'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.2.70:kotlin-scripting-compiler-embeddable-1.2.70.jar:cbd88e1cae3f8f2baeb7021d3b76323b30e82663e7b4222a214f33ee314b3653',
'org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70:kotlin-stdlib-common-1.2.70.jar:bb6898bef18e1de5d416d5135ca70dcac6645718c7d84bbddcfeb76ed1c199a1',
'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70:kotlin-stdlib-jdk7-1.2.70.jar:b4ace315288134b52fddb58b4a92636faafb2ab5eb46bad97d3bce7623a8e213',
'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70:kotlin-stdlib-jdk8-1.2.70.jar:88d0c29f4065b6ad34261fb4a04d39f9813051c6942428d718b649378d057ad1',
'org.jetbrains.kotlin:kotlin-stdlib:1.2.70:kotlin-stdlib-1.2.70.jar:7d20d0a56dd0ea6176137759a6aad331bbfae67436b45e5f0a4d8dafb6985c81',
'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478',
'org.junit.jupiter:junit-jupiter-api:5.2.0:junit-jupiter-api-5.2.0.jar:47f7d71b35dc331210b9ab219bbb00d54332981aa12eb5effe817de17e1ae7b3',
'org.junit.jupiter:junit-jupiter-engine:5.2.0:junit-jupiter-engine-5.2.0.jar:8f994f4094790e246dc84de86a1ff4194ca85e8b13bedaca0207f727ebfbc813',
'org.junit.jupiter:junit-jupiter-params:5.2.0:junit-jupiter-params-5.2.0.jar:34ce02519044ef68217002f640a83e267c4001ce53b68270218d49d00449a836',
'org.junit.platform:junit-platform-commons:1.2.0:junit-platform-commons-1.2.0.jar:7771af2f797d1d0ccce9920eb3cd826fb8fd7659ccb4d8877e76d9412be72cc2',
'org.junit.platform:junit-platform-engine:1.2.0:junit-platform-engine-1.2.0.jar:60b102e94ea01556fdc8c041950a05450edc188e3708f032a6bfb1a50ba0bc22',
'org.objenesis:objenesis:2.6:objenesis-2.6.jar:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d',
'org.opentest4j:opentest4j:1.1.0:opentest4j-1.1.0.jar:65a5fd7380f53aac708bcee3091dbe2dba73a9a2e7645b66e70e0804fc36ee3b',
'org.skyscreamer:jsonassert:1.5.0:jsonassert-1.5.0.jar:a310bc79c3f4744e2b2e993702fcebaf3696fec0063643ffdc6b49a8fb03ef39',
'org.slf4j:slf4j-api:1.7.25:slf4j-api-1.7.25.jar:18c4a0095d5c1da6b817592e767bb23d29dd2f560ad74df75ff3961dbde25b79',
'org.slf4j:slf4j-simple:1.7.25:slf4j-simple-1.7.25.jar:0966e86fffa5be52d3d9e7b89dd674d98a03eed0a454fbaf7c1bd9493bd9d874',
]
}

View File

@@ -5,3 +5,4 @@ include ':bramble-java'
include ':briar-api'
include ':briar-core'
include ':briar-android'
include ':briar-headless'

View File

@@ -9,6 +9,7 @@ PROJECTS=(
'briar-api'
'briar-core'
'briar-android'
'briar-headless'
)
# clear witness files to prevent errors when upgrading dependencies