mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 03:09:04 +01:00
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:
9
.idea/codeStyles/Project.xml
generated
9
.idea/codeStyles/Project.xml
generated
@@ -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>
|
||||
23
.idea/runConfigurations/All_in_briar_headless.xml
generated
Normal file
23
.idea/runConfigurations/All_in_briar_headless.xml
generated
Normal 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>
|
||||
3
.idea/runConfigurations/All_tests.xml
generated
3
.idea/runConfigurations/All_tests.xml
generated
@@ -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>
|
||||
17
.idea/runConfigurations/briar_headless.xml
generated
Normal file
17
.idea/runConfigurations/briar_headless.xml
generated
Normal 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
197
briar-headless/README.md
Normal 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.
|
||||
60
briar-headless/build.gradle
Normal file
60
briar-headless/build.gradle
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.briarproject.briar.headless.contact
|
||||
|
||||
import io.javalin.Context
|
||||
|
||||
interface ContactController {
|
||||
|
||||
fun list(ctx: Context): Context
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
}
|
||||
61
briar-headless/witness.gradle
Normal file
61
briar-headless/witness.gradle
Normal 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',
|
||||
]
|
||||
}
|
||||
@@ -5,3 +5,4 @@ include ':bramble-java'
|
||||
include ':briar-api'
|
||||
include ':briar-core'
|
||||
include ':briar-android'
|
||||
include ':briar-headless'
|
||||
|
||||
@@ -9,6 +9,7 @@ PROJECTS=(
|
||||
'briar-api'
|
||||
'briar-core'
|
||||
'briar-android'
|
||||
'briar-headless'
|
||||
)
|
||||
|
||||
# clear witness files to prevent errors when upgrading dependencies
|
||||
|
||||
Reference in New Issue
Block a user