Merge branch '1256-adding-contacts-headless' into 'master'

Add a REST endpoint for adding contacts

Closes #1256

See merge request briar/briar!1094
This commit is contained in:
akwizgran
2019-05-16 14:05:48 +00:00
20 changed files with 686 additions and 62 deletions

View File

@@ -5,6 +5,7 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.PendingContact; import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactId; import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.contact.PendingContactState;
import org.briarproject.bramble.api.crypto.AgreementPrivateKey; import org.briarproject.bramble.api.crypto.AgreementPrivateKey;
import org.briarproject.bramble.api.crypto.AgreementPublicKey; import org.briarproject.bramble.api.crypto.AgreementPublicKey;
import org.briarproject.bramble.api.crypto.PrivateKey; import org.briarproject.bramble.api.crypto.PrivateKey;
@@ -36,7 +37,6 @@ import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_PUBLIC_KEY_BYTES; import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_PUBLIC_KEY_BYTES;
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION; import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
@@ -181,8 +181,10 @@ public class TestUtils {
PendingContactId id = new PendingContactId(getRandomId()); PendingContactId id = new PendingContactId(getRandomId());
PublicKey publicKey = getAgreementPublicKey(); PublicKey publicKey = getAgreementPublicKey();
String alias = getRandomString(nameLength); String alias = getRandomString(nameLength);
return new PendingContact(id, publicKey, alias, WAITING_FOR_CONNECTION, int stateIndex =
timestamp); random.nextInt(PendingContactState.values().length - 1);
PendingContactState state = PendingContactState.values()[stateIndex];
return new PendingContact(id, publicKey, alias, state, timestamp);
} }
public static ContactId getContactId() { public static ContactId getContactId() {

View File

@@ -7,6 +7,8 @@ import org.briarproject.bramble.api.contact.PendingContactId;
import org.briarproject.bramble.api.contact.event.ContactAddedEvent; import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent; import org.briarproject.bramble.api.contact.event.ContactVerifiedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent;
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent;
import org.briarproject.bramble.api.crypto.PrivateKey; import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
@@ -293,6 +295,8 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (db.containsPendingContact(txn, p.getId())) if (db.containsPendingContact(txn, p.getId()))
throw new PendingContactExistsException(); throw new PendingContactExistsException();
db.addPendingContact(txn, p); db.addPendingContact(txn, p);
transaction.attach(new PendingContactStateChangedEvent(p.getId(),
p.getState()));
} }
@Override @Override
@@ -892,6 +896,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
if (!db.containsPendingContact(txn, p)) if (!db.containsPendingContact(txn, p))
throw new NoSuchPendingContactException(); throw new NoSuchPendingContactException();
db.removePendingContact(txn, p); db.removePendingContact(txn, p);
transaction.attach(new PendingContactRemovedEvent(p));
} }
@Override @Override

View File

@@ -30,3 +30,15 @@ dependencies {
signature 'org.codehaus.mojo.signature:java16:1.1@signature' signature 'org.codehaus.mojo.signature:java16:1.1@signature'
} }
// needed to make test output available to briar-headless
configurations {
testOutput.extendsFrom(testCompile)
}
task jarTest(type: Jar, dependsOn: testClasses) {
from sourceSets.test.output
classifier = 'test'
}
artifacts {
testOutput jarTest
}

View File

@@ -1,13 +1,19 @@
package org.briarproject.briar.test; package org.briarproject.briar.test;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.KeyPair;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorFactory; import org.briarproject.bramble.api.identity.AuthorFactory;
import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.util.Base32;
import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount; import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import static java.lang.System.arraycopy;
import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.FORMAT_VERSION;
import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.RAW_LINK_BYTES;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@@ -40,4 +46,13 @@ public class BriarTestUtils {
return authorFactory.createLocalAuthor(name); return authorFactory.createLocalAuthor(name);
} }
public static String getRealHandshakeLink(CryptoComponent cryptoComponent) {
KeyPair keyPair = cryptoComponent.generateAgreementKeyPair();
byte[] linkBytes = new byte[RAW_LINK_BYTES];
byte[] publicKey = keyPair.getPublic().getEncoded();
linkBytes[0] = FORMAT_VERSION;
arraycopy(publicKey,0, linkBytes, 1, RAW_LINK_BYTES - 1);
return ("briar://" + Base32.encode(linkBytes)).toLowerCase();
}
} }

View File

@@ -65,16 +65,87 @@ Returns a JSON array of contacts:
"publicKey": "BDu6h1S02bF4W6rgoZfZ6BMjTj/9S9hNN7EQoV05qUo=" "publicKey": "BDu6h1S02bF4W6rgoZfZ6BMjTj/9S9hNN7EQoV05qUo="
}, },
"contactId": 1, "contactId": 1,
"alias" : "A local nickname",
"handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=",
"verified": true "verified": true
} }
``` ```
### Adding a contact ### Adding a contact
*Not yet implemented* The first step is to get your own link:
The only workaround is to add a contact to the Briar app running on a rooted Android phone `GET /v1/contacts/add/link`
and then move its database (and key files) to the headless peer.
Returns a JSON object with a `briar://` link that needs to be sent to the contact you want to add
outside of Briar via an external channel.
```json
{
"link": "briar://wvui4uvhbfv4tzo6xwngknebsxrafainnhldyfj63x6ipp4q2vigy"
}
```
Once you have received the link of your future contact, you can add them
by posting the link together with an arbitrary nickname (or alias):
`POST /v1/contacts/add/pending`
The link and the alias should be posted as a JSON object:
```json
{
"link": "briar://ddnsyffpsenoc3yzlhr24aegfq2pwan7kkselocill2choov6sbhs",
"alias": "A nickname for the new contact"
}
```
This starts the process of adding the contact.
Until it is completed, a pending contact is returned as JSON:
```json
{
"pendingContactId": "jsTgWcsEQ2g9rnomeK1g/hmO8M1Ix6ZIGWAjgBtlS9U=",
"alias": "ztatsaajzeegraqcizbbfftofdekclatyht",
"state": "adding_contact",
"timestamp": 1557838312175
}
```
The state can be one of these values:
* `waiting_for_connection`
* `connected`
* `adding_contact`
* `failed`
If you want to get informed about state changes,
you can use the Websocket API (below) to listen for events.
The following events are relevant here:
* `PendingContactStateChangedEvent`
* `PendingContactRemovedEvent`
* `ContactAddedRemotelyEvent` (when the pending contact becomes an actual contact)
It is possible to get a list of all pending contacts:
`GET /v1/contacts/add/pending`
This will return a JSON array of pending contacts formatted as shown above.
To remove a pending contact and abort the process of adding it:
`DELETE /v1/contacts/add/pending`
The `pendingContactId` of the pending contact to delete
needs to be provided in the request body as follows:
```json
{
"pendingContactId": "jsTgWcsEQ2g9rnomeK1g/hmO8M1Ix6ZIGWAjgBtlS9U="
}
```
### Removing a contact ### Removing a contact
@@ -204,3 +275,56 @@ it will send a JSON object to connected websocket clients:
Note that the JSON object in `data` is exactly what the REST API returns Note that the JSON object in `data` is exactly what the REST API returns
when listing private messages. when listing private messages.
### A new contact was added remotely
When the Briar peer adds a new contact remotely,
it will send a JSON object representing the new contact to connected websocket clients:
```json
{
"data": {
"contact": {
"author": {
"formatVersion": 1,
"id": "y1wkIzAimAbYoCGgWxkWlr6vnq1F8t1QRA/UMPgI0E0=",
"name": "Test",
"publicKey": "BDu6h1S02bF4W6rgoZfZ6BMjTj/9S9hNN7EQoV05qUo="
},
"contactId": 1,
"alias" : "A local nickname",
"handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=",
"verified": true
}
},
"name": "ContactAddedRemotelyEvent",
"type": "event"
}
```
### A pending contact changed its state
```json
{
"data": {
"pendingContactId":"YqKjsczCuxScXohb5+RAYtFEwK71icoB4ldztV2gh7M=",
"state":"waiting_for_connection"
},
"name": "PendingContactStateChangedEvent",
"type": "event"
}
```
For a list of valid states, please see the section on adding contacts above.
### A pending contact was removed
```json
{
"data": {
"pendingContactId": "YqKjsczCuxScXohb5+RAYtFEwK71icoB4ldztV2gh7M="
},
"name": "PendingContactRemovedEvent",
"type": "event"
}
```

View File

@@ -1,8 +1,8 @@
plugins { plugins {
id 'java' id 'java'
id 'idea' id 'idea'
id 'org.jetbrains.kotlin.jvm' version '1.3.21' id 'org.jetbrains.kotlin.jvm' version '1.3.31'
id 'org.jetbrains.kotlin.kapt' version '1.3.21' id 'org.jetbrains.kotlin.kapt' version '1.3.31'
id 'witness' id 'witness'
} }
apply from: 'witness.gradle' apply from: 'witness.gradle'
@@ -14,25 +14,28 @@ dependencies {
implementation project(path: ':briar-core', configuration: 'default') implementation project(path: ':briar-core', configuration: 'default')
implementation project(path: ':bramble-java', configuration: 'default') implementation project(path: ':bramble-java', configuration: 'default')
implementation 'io.javalin:javalin:2.7.0' implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.31'
implementation 'org.slf4j:slf4j-simple:1.7.25' implementation 'io.javalin:javalin:2.8.0'
implementation 'org.slf4j:slf4j-simple:1.7.26'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8'
implementation 'com.github.ajalt:clikt:1.6.0' implementation 'com.github.ajalt:clikt:2.0.0'
kapt 'com.google.dagger:dagger-compiler:2.22.1' def daggerVersion = '2.22.1'
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
testImplementation project(path: ':bramble-api', configuration: 'testOutput') testImplementation project(path: ':bramble-api', configuration: 'testOutput')
testImplementation project(path: ':bramble-core', configuration: 'testOutput') testImplementation project(path: ':bramble-core', configuration: 'testOutput')
testImplementation project(path: ':briar-core', configuration: 'testOutput')
def junitVersion = '5.3.1' def junitVersion = '5.4.2'
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion" testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testImplementation "io.mockk:mockk:1.9.2" testImplementation 'io.mockk:mockk:1.9.3'
testImplementation "org.skyscreamer:jsonassert:1.5.0" testImplementation 'org.skyscreamer:jsonassert:1.5.0'
testImplementation 'khttp:khttp:0.1.0' testImplementation 'khttp:khttp:0.1.0'
kaptTest 'com.google.dagger:dagger-compiler:2.22.1' kaptTest "com.google.dagger:dagger-compiler:$daggerVersion"
} }
jar { jar {

View File

@@ -8,7 +8,7 @@ fun Author.output() = JsonDict(
"formatVersion" to formatVersion, "formatVersion" to formatVersion,
"id" to id.bytes, "id" to id.bytes,
"name" to name, "name" to name,
"publicKey" to publicKey "publicKey" to publicKey.encoded
) )
fun AuthorInfo.Status.output() = name.toLowerCase() fun AuthorInfo.Status.output() = name.toLowerCase()

View File

@@ -64,6 +64,16 @@ constructor(
path("/v1") { path("/v1") {
path("/contacts") { path("/contacts") {
get { ctx -> contactController.list(ctx) } get { ctx -> contactController.list(ctx) }
path("add") {
path("link") {
get { ctx -> contactController.getLink(ctx) }
}
path("pending") {
get { ctx -> contactController.listPendingContacts(ctx) }
post { ctx -> contactController.addPendingContact(ctx) }
delete { ctx -> contactController.removePendingContact(ctx) }
}
}
path("/:contactId") { path("/:contactId") {
delete { ctx -> contactController.delete(ctx) } delete { ctx -> contactController.delete(ctx) }
} }

View File

@@ -5,6 +5,10 @@ import io.javalin.Context
interface ContactController { interface ContactController {
fun list(ctx: Context): Context fun list(ctx: Context): Context
fun getLink(ctx: Context): Context
fun addPendingContact(ctx: Context): Context
fun listPendingContacts(ctx: Context): Context
fun removePendingContact(ctx: Context): Context
fun delete(ctx: Context): Context fun delete(ctx: Context): Context
} }

View File

@@ -1,19 +1,58 @@
package org.briarproject.briar.headless.contact package org.briarproject.briar.headless.contact
import com.fasterxml.jackson.databind.ObjectMapper
import io.javalin.BadRequestResponse
import io.javalin.Context import io.javalin.Context
import io.javalin.NotFoundResponse import io.javalin.NotFoundResponse
import org.briarproject.bramble.api.contact.ContactManager import org.briarproject.bramble.api.contact.ContactManager
import org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX
import org.briarproject.bramble.api.contact.PendingContactId
import org.briarproject.bramble.api.contact.event.ContactAddedRemotelyEvent
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent
import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.api.db.NoSuchPendingContactException
import org.briarproject.bramble.api.event.Event
import org.briarproject.bramble.api.event.EventListener
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
import org.briarproject.bramble.util.StringUtils.toUtf8
import org.briarproject.briar.headless.event.WebSocketController
import org.briarproject.briar.headless.getContactIdFromPathParam import org.briarproject.briar.headless.getContactIdFromPathParam
import org.briarproject.briar.headless.getFromJson
import org.briarproject.briar.headless.json.JsonDict
import org.spongycastle.util.encoders.Base64
import org.spongycastle.util.encoders.DecoderException
import javax.annotation.concurrent.Immutable import javax.annotation.concurrent.Immutable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
internal const val EVENT_CONTACT_ADDED_REMOTELY = "ContactAddedRemotelyEvent"
internal const val EVENT_PENDING_CONTACT_STATE_CHANGED = "PendingContactStateChangedEvent"
internal const val EVENT_PENDING_CONTACT_REMOVED = "PendingContactRemovedEvent"
@Immutable @Immutable
@Singleton @Singleton
internal class ContactControllerImpl internal class ContactControllerImpl
@Inject @Inject
constructor(private val contactManager: ContactManager) : ContactController { constructor(
private val contactManager: ContactManager,
private val objectMapper: ObjectMapper,
private val webSocket: WebSocketController
) : ContactController, EventListener {
override fun eventOccurred(e: Event) = when (e) {
is ContactAddedRemotelyEvent -> {
webSocket.sendEvent(EVENT_CONTACT_ADDED_REMOTELY, e.output())
}
is PendingContactStateChangedEvent -> {
webSocket.sendEvent(EVENT_PENDING_CONTACT_STATE_CHANGED, e.output())
}
is PendingContactRemovedEvent -> {
webSocket.sendEvent(EVENT_PENDING_CONTACT_REMOVED, e.output())
}
else -> {
}
}
override fun list(ctx: Context): Context { override fun list(ctx: Context): Context {
val contacts = contactManager.contacts.map { contact -> val contacts = contactManager.contacts.map { contact ->
@@ -22,6 +61,48 @@ constructor(private val contactManager: ContactManager) : ContactController {
return ctx.json(contacts) return ctx.json(contacts)
} }
override fun getLink(ctx: Context): Context {
val linkDict = JsonDict("link" to contactManager.handshakeLink)
return ctx.json(linkDict)
}
override fun addPendingContact(ctx: Context): Context {
val link = ctx.getFromJson(objectMapper, "link")
val alias = ctx.getFromJson(objectMapper, "alias")
if (!LINK_REGEX.matcher(link).find()) throw BadRequestResponse("Invalid Link")
val aliasUtf8 = toUtf8(alias)
if (aliasUtf8.isEmpty() || aliasUtf8.size > MAX_AUTHOR_NAME_LENGTH)
throw BadRequestResponse("Invalid Alias")
val pendingContact = contactManager.addPendingContact(link, alias)
return ctx.json(pendingContact.output())
}
override fun listPendingContacts(ctx: Context): Context {
val pendingContacts = contactManager.pendingContacts.map { pendingContact ->
pendingContact.output()
}
return ctx.json(pendingContacts)
}
override fun removePendingContact(ctx: Context): Context {
// construct and check PendingContactId
val pendingContactString = ctx.getFromJson(objectMapper, "pendingContactId")
val pendingContactBytes = try {
Base64.decode(pendingContactString)
} catch (e: DecoderException) {
throw NotFoundResponse()
}
if (pendingContactBytes.size != PendingContactId.LENGTH) throw NotFoundResponse()
val id = PendingContactId(pendingContactBytes)
// remove
try {
contactManager.removePendingContact(id)
} catch (e: NoSuchPendingContactException) {
throw NotFoundResponse()
}
return ctx
}
override fun delete(ctx: Context): Context { override fun delete(ctx: Context): Context {
val contactId = ctx.getContactIdFromPathParam() val contactId = ctx.getContactIdFromPathParam()
try { try {

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.headless.contact
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.briarproject.bramble.api.event.EventBus
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -9,7 +10,11 @@ class HeadlessContactModule {
@Provides @Provides
@Singleton @Singleton
internal fun provideContactController(contactController: ContactControllerImpl): ContactController { internal fun provideContactController(
eventBus: EventBus,
contactController: ContactControllerImpl
): ContactController {
eventBus.addListener(contactController)
return contactController return contactController
} }

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.headless.contact package org.briarproject.briar.headless.contact
import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.event.ContactAddedRemotelyEvent
import org.briarproject.bramble.identity.output import org.briarproject.bramble.identity.output
import org.briarproject.briar.headless.json.JsonDict import org.briarproject.briar.headless.json.JsonDict
@@ -8,4 +9,11 @@ internal fun Contact.output() = JsonDict(
"contactId" to id.int, "contactId" to id.int,
"author" to author.output(), "author" to author.output(),
"verified" to isVerified "verified" to isVerified
) ).apply {
alias?.let { put("alias", it) }
handshakePublicKey?.let { put("handshakePublicKey", it.encoded) }
}
internal fun ContactAddedRemotelyEvent.output() = JsonDict(
"contact" to contact.output()
)

View File

@@ -0,0 +1,32 @@
package org.briarproject.briar.headless.contact
import org.briarproject.bramble.api.contact.PendingContact
import org.briarproject.bramble.api.contact.PendingContactState
import org.briarproject.bramble.api.contact.PendingContactState.*
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent
import org.briarproject.briar.headless.json.JsonDict
internal fun PendingContact.output() = JsonDict(
"pendingContactId" to id.bytes,
"alias" to alias,
"state" to state.output(),
"timestamp" to timestamp
)
internal fun PendingContactState.output() = when(this) {
WAITING_FOR_CONNECTION -> "waiting_for_connection"
CONNECTED -> "connected"
ADDING_CONTACT -> "adding_contact"
FAILED -> "failed"
else -> throw AssertionError()
}
internal fun PendingContactStateChangedEvent.output() = JsonDict(
"pendingContactId" to id.bytes,
"state" to pendingContactState.output()
)
internal fun PendingContactRemovedEvent.output() = JsonDict(
"pendingContactId" to id.bytes
)

View File

@@ -4,6 +4,7 @@ import dagger.Component
import org.briarproject.bramble.BrambleCoreEagerSingletons import org.briarproject.bramble.BrambleCoreEagerSingletons
import org.briarproject.bramble.BrambleCoreModule import org.briarproject.bramble.BrambleCoreModule
import org.briarproject.bramble.account.AccountModule import org.briarproject.bramble.account.AccountModule
import org.briarproject.bramble.api.crypto.CryptoComponent
import org.briarproject.bramble.event.DefaultEventExecutorModule import org.briarproject.bramble.event.DefaultEventExecutorModule
import org.briarproject.bramble.test.TestSecureRandomModule import org.briarproject.bramble.test.TestSecureRandomModule
import org.briarproject.briar.BriarCoreEagerSingletons import org.briarproject.briar.BriarCoreEagerSingletons
@@ -25,5 +26,7 @@ import javax.inject.Singleton
internal interface BriarHeadlessTestApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons { internal interface BriarHeadlessTestApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons {
fun getRouter(): Router fun getRouter(): Router
fun getCryptoComponent(): CryptoComponent
fun getTestDataCreator(): TestDataCreator fun getTestDataCreator(): TestDataCreator
} }

View File

@@ -14,6 +14,7 @@ import org.briarproject.bramble.api.sync.Message
import org.briarproject.bramble.api.system.Clock import org.briarproject.bramble.api.system.Clock
import org.briarproject.bramble.test.TestUtils.* import org.briarproject.bramble.test.TestUtils.*
import org.briarproject.bramble.util.StringUtils.getRandomString import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.headless.event.WebSocketController
import org.skyscreamer.jsonassert.JSONAssert.assertEquals import org.skyscreamer.jsonassert.JSONAssert.assertEquals
import org.skyscreamer.jsonassert.JSONCompareMode.STRICT import org.skyscreamer.jsonassert.JSONCompareMode.STRICT
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@@ -26,6 +27,8 @@ abstract class ControllerTest {
protected val clock = mockk<Clock>() protected val clock = mockk<Clock>()
protected val ctx = mockk<Context>() protected val ctx = mockk<Context>()
protected val webSocketController = mockk<WebSocketController>()
private val request = mockk<HttpServletRequest>(relaxed = true) private val request = mockk<HttpServletRequest>(relaxed = true)
private val response = mockk<HttpServletResponse>(relaxed = true) private val response = mockk<HttpServletResponse>(relaxed = true)
private val outputCtx = ContextUtil.init(request, response) private val outputCtx = ContextUtil.init(request, response)

View File

@@ -4,6 +4,7 @@ import io.javalin.Javalin
import io.javalin.core.util.Header.AUTHORIZATION import io.javalin.core.util.Header.AUTHORIZATION
import khttp.responses.Response import khttp.responses.Response
import org.briarproject.bramble.BrambleCoreModule import org.briarproject.bramble.BrambleCoreModule
import org.briarproject.bramble.api.crypto.CryptoComponent
import org.briarproject.briar.BriarCoreModule import org.briarproject.briar.BriarCoreModule
import org.briarproject.briar.api.test.TestDataCreator import org.briarproject.briar.api.test.TestDataCreator
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
@@ -22,6 +23,7 @@ abstract class IntegrationTest {
private val dataDir = File("tmp") private val dataDir = File("tmp")
protected lateinit var api: Javalin protected lateinit var api: Javalin
protected lateinit var crypto: CryptoComponent
protected lateinit var testDataCreator: TestDataCreator protected lateinit var testDataCreator: TestDataCreator
private lateinit var router: Router private lateinit var router: Router
@@ -33,6 +35,7 @@ abstract class IntegrationTest {
BrambleCoreModule.initEagerSingletons(app) BrambleCoreModule.initEagerSingletons(app)
BriarCoreModule.initEagerSingletons(app) BriarCoreModule.initEagerSingletons(app)
router = app.getRouter() router = app.getRouter()
crypto = app.getCryptoComponent()
testDataCreator = app.getTestDataCreator() testDataCreator = app.getTestDataCreator()
api = router.start(token, port, false) api = router.start(token, port, false)
@@ -52,10 +55,22 @@ abstract class IntegrationTest {
return khttp.get(url, getAuthTokenHeader("wrongToken")) return khttp.get(url, getAuthTokenHeader("wrongToken"))
} }
protected fun post(url: String, data: String) : Response {
return khttp.post(url, getAuthTokenHeader(token), data = data)
}
protected fun postWithWrongToken(url: String) : Response {
return khttp.post(url, getAuthTokenHeader("wrongToken"), data = "")
}
protected fun delete(url: String) : Response { protected fun delete(url: String) : Response {
return khttp.delete(url, getAuthTokenHeader(token)) return khttp.delete(url, getAuthTokenHeader(token))
} }
protected fun delete(url: String, data: String) : Response {
return khttp.delete(url, getAuthTokenHeader(token), data = data)
}
protected fun deleteWithWrongToken(url: String) : Response { protected fun deleteWithWrongToken(url: String) : Response {
return khttp.delete(url, getAuthTokenHeader("wrongToken")) return khttp.delete(url, getAuthTokenHeader("wrongToken"))
} }

View File

@@ -1,8 +1,11 @@
package org.briarproject.briar.headless.contact package org.briarproject.briar.headless.contact
import org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES
import org.briarproject.briar.headless.IntegrationTest import org.briarproject.briar.headless.IntegrationTest
import org.briarproject.briar.headless.url import org.briarproject.briar.headless.url
import org.briarproject.briar.test.BriarTestUtils.getRealHandshakeLink
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
class ContactControllerIntegrationTest: IntegrationTest() { class ContactControllerIntegrationTest: IntegrationTest() {
@@ -33,6 +36,75 @@ class ContactControllerIntegrationTest: IntegrationTest() {
assertEquals(testContactName, author.getString("name")) assertEquals(testContactName, author.getString("name"))
} }
@Test
fun `returns own handshake link`() {
val response = get("$url/contacts/add/link")
assertEquals(200, response.statusCode)
val link = response.jsonObject.getString("link")
assertTrue(link.startsWith("briar://"))
assertEquals(BASE32_LINK_BYTES + 8, link.length)
}
@Test
fun `returning own handshake link needs authentication token`() {
val response = getWithWrongToken("$url/contacts/add/link")
assertEquals(401, response.statusCode)
}
@Test
fun `returns list of pending contacts`() {
// retrieve empty list of pending contacts
var response = get("$url/contacts/add/pending")
assertEquals(200, response.statusCode)
assertEquals(0, response.jsonArray.length())
// add one pending contact
val alias = "AliasFoo"
val json = """{
"link": "${getRealHandshakeLink(crypto)}",
"alias": "$alias"
}"""
response = post("$url/contacts/add/pending", json)
assertEquals(200, response.statusCode)
// get added contact as only list item
response = get("$url/contacts/add/pending")
assertEquals(200, response.statusCode)
assertEquals(1, response.jsonArray.length())
val jsonObject = response.jsonArray.getJSONObject(0)
assertEquals(alias, jsonObject.getString("alias"))
assertEquals("waiting_for_connection", jsonObject.getString("state"))
// remove pending contact again
val idString = jsonObject.getString("pendingContactId")
val deleteJson = """{"pendingContactId": "$idString"}"""
response = delete("$url/contacts/add/pending", deleteJson)
assertEquals(200, response.statusCode)
// list of pending contacts should be empty now
response = get("$url/contacts/add/pending")
assertEquals(200, response.statusCode)
assertEquals(0, response.jsonArray.length())
}
@Test
fun `returning list of pending contacts needs authentication token`() {
val response = getWithWrongToken("$url/contacts/add/pending")
assertEquals(401, response.statusCode)
}
@Test
fun `adding pending contacts needs authentication token`() {
val response = postWithWrongToken("$url/contacts/add/pending")
assertEquals(401, response.statusCode)
}
@Test
fun `removing a pending contact needs authentication token`() {
val response = deleteWithWrongToken("$url/contacts/add/pending")
assertEquals(401, response.statusCode)
}
@Test @Test
fun `deleting contact need authentication token`() { fun `deleting contact need authentication token`() {
val response = deleteWithWrongToken("$url/contacts/1") val response = deleteWithWrongToken("$url/contacts/1")

View File

@@ -1,21 +1,38 @@
package org.briarproject.briar.headless.contact package org.briarproject.briar.headless.contact
import io.javalin.BadRequestResponse
import io.javalin.NotFoundResponse import io.javalin.NotFoundResponse
import io.javalin.json.JavalinJson.toJson import io.javalin.json.JavalinJson.toJson
import io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.runs
import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.Contact
import org.briarproject.bramble.api.contact.ContactId import org.briarproject.bramble.api.contact.ContactId
import org.briarproject.bramble.api.contact.PendingContactId
import org.briarproject.bramble.api.contact.PendingContactState.FAILED
import org.briarproject.bramble.api.contact.event.ContactAddedRemotelyEvent
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent
import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent
import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.db.NoSuchContactException
import org.briarproject.bramble.api.db.NoSuchPendingContactException
import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH
import org.briarproject.bramble.identity.output import org.briarproject.bramble.identity.output
import org.briarproject.bramble.test.TestUtils.getPendingContact
import org.briarproject.bramble.test.TestUtils.getRandomBytes
import org.briarproject.bramble.util.StringUtils.getRandomString
import org.briarproject.briar.headless.ControllerTest import org.briarproject.briar.headless.ControllerTest
import org.briarproject.briar.headless.json.JsonDict
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class ContactControllerTest : ControllerTest() { internal class ContactControllerTest : ControllerTest() {
private val controller = ContactControllerImpl(contactManager) private val pendingContact = getPendingContact()
private val controller =
ContactControllerImpl(contactManager, objectMapper, webSocketController)
@Test @Test
fun testEmptyContactList() { fun testEmptyContactList() {
@@ -31,6 +48,131 @@ internal class ContactControllerTest : ControllerTest() {
controller.list(ctx) controller.list(ctx)
} }
@Test
fun testLink() {
val link = "briar://link"
every { contactManager.handshakeLink } returns link
every { ctx.json(JsonDict("link" to link)) } returns ctx
controller.getLink(ctx)
}
@Test
fun testAddPendingContact() {
val link = "briar://briar://adnsyffpsenoc3yzlhr24aegfq2pwan7kkselocill2choov6sbhs"
val alias = "Alias123"
val body = """{
"link": "$link",
"alias": "$alias"
}"""
every { ctx.body() } returns body
every { contactManager.addPendingContact(link, alias) } returns pendingContact
every { ctx.json(pendingContact.output()) } returns ctx
controller.addPendingContact(ctx)
}
@Test
fun testAddPendingContactInvalidLink() {
val link = "briar://link123"
val alias = "Alias123"
val body = """{
"link": "$link",
"alias": "$alias"
}"""
every { ctx.body() } returns body
assertThrows(BadRequestResponse::class.java) {
controller.addPendingContact(ctx)
}
}
@Test
fun testAddPendingContactMissingLink() {
val alias = "Alias123"
val body = """{
"alias": "$alias"
}"""
every { ctx.body() } returns body
assertThrows(BadRequestResponse::class.java) {
controller.addPendingContact(ctx)
}
}
@Test
fun testAddPendingContactInvalidAlias() {
val link = "briar://briar://adnsyffpsenoc3yzlhr24aegfq2pwan7kkselocill2choov6sbhs"
val alias = getRandomString(MAX_AUTHOR_NAME_LENGTH + 1)
val body = """{
"link": "$link",
"alias": "$alias"
}"""
every { ctx.body() } returns body
assertThrows(BadRequestResponse::class.java) {
controller.addPendingContact(ctx)
}
}
@Test
fun testAddPendingContactMissingAlias() {
val link = "briar://adnsyffpsenoc3yzlhr24aegfq2pwan7kkselocill2choov6sbhs"
val body = """{
"link": "$link"
}"""
every { ctx.body() } returns body
assertThrows(BadRequestResponse::class.java) {
controller.addPendingContact(ctx)
}
}
@Test
fun testListPendingContacts() {
every { contactManager.pendingContacts } returns listOf(pendingContact)
every { ctx.json(listOf(pendingContact.output())) } returns ctx
controller.listPendingContacts(ctx)
}
@Test
fun testRemovePendingContact() {
val id = pendingContact.id
every { ctx.body() } returns """{"pendingContactId": ${toJson(id.bytes)}}"""
every { contactManager.removePendingContact(id) } just Runs
controller.removePendingContact(ctx)
}
@Test
fun testRemovePendingContactInvalidId() {
every { ctx.body() } returns """{"pendingContactId": "foo"}"""
assertThrows(NotFoundResponse::class.java) {
controller.removePendingContact(ctx)
}
}
@Test
fun testRemovePendingContactTooShortId() {
val bytes = getRandomBytes(PendingContactId.LENGTH - 1)
every { ctx.body() } returns """{"pendingContactId": ${toJson(bytes)}}"""
assertThrows(NotFoundResponse::class.java) {
controller.removePendingContact(ctx)
}
}
@Test
fun testRemovePendingContactTooLongId() {
val bytes = getRandomBytes(PendingContactId.LENGTH + 1)
every { ctx.body() } returns """{"pendingContactId": ${toJson(bytes)}}"""
assertThrows(NotFoundResponse::class.java) {
controller.removePendingContact(ctx)
}
}
@Test
fun testRemovePendingContactNonexistentId() {
val id = pendingContact.id
every { ctx.body() } returns """{"pendingContactId": ${toJson(id.bytes)}}"""
every { contactManager.removePendingContact(id) } throws NoSuchPendingContactException()
assertThrows(NotFoundResponse::class.java) {
controller.removePendingContact(ctx)
}
}
@Test @Test
fun testDelete() { fun testDelete() {
every { ctx.pathParam("contactId") } returns "1" every { ctx.pathParam("contactId") } returns "1"
@@ -55,12 +197,57 @@ internal class ContactControllerTest : ControllerTest() {
} }
} }
@Test
fun testContactAddedRemotelyEvent() {
val event = ContactAddedRemotelyEvent(contact)
every {
webSocketController.sendEvent(
EVENT_CONTACT_ADDED_REMOTELY,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test
fun testPendingContactStateChangedEvent() {
val event = PendingContactStateChangedEvent(pendingContact.id, FAILED)
every {
webSocketController.sendEvent(
EVENT_PENDING_CONTACT_STATE_CHANGED,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test
fun testPendingContactRemovedEvent() {
val event = PendingContactRemovedEvent(pendingContact.id)
every {
webSocketController.sendEvent(
EVENT_PENDING_CONTACT_REMOVED,
event.output()
)
} just runs
controller.eventOccurred(event)
}
@Test @Test
fun testOutputContact() { fun testOutputContact() {
assertNotNull(contact.handshakePublicKey)
val json = """ val json = """
{ {
"contactId": ${contact.id.int}, "contactId": ${contact.id.int},
"author": ${toJson(author.output())}, "author": ${toJson(author.output())},
"alias" : "${contact.alias}",
"handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)},
"verified": ${contact.isVerified} "verified": ${contact.isVerified}
} }
""" """
@@ -74,10 +261,57 @@ internal class ContactControllerTest : ControllerTest() {
"formatVersion": 1, "formatVersion": 1,
"id": ${toJson(author.id.bytes)}, "id": ${toJson(author.id.bytes)},
"name": "${author.name}", "name": "${author.name}",
"publicKey": ${toJson(author.publicKey)} "publicKey": ${toJson(author.publicKey.encoded)}
} }
""" """
assertJsonEquals(json, author.output()) assertJsonEquals(json, author.output())
} }
@Test
fun testOutputContactAddedRemotelyEvent() {
val event = ContactAddedRemotelyEvent(contact)
val json = """
{
"contact": ${toJson(contact.output())}
}
"""
assertJsonEquals(json, event.output())
}
@Test
fun testOutputPendingContact() {
val json = """
{
"pendingContactId": ${toJson(pendingContact.id.bytes)},
"alias": "${pendingContact.alias}",
"state": "${pendingContact.state.name.toLowerCase()}",
"timestamp": ${pendingContact.timestamp}
}
"""
assertJsonEquals(json, pendingContact.output())
}
@Test
fun testOutputPendingContactStateChangedEvent() {
val event = PendingContactStateChangedEvent(pendingContact.id, FAILED)
val json = """
{
"pendingContactId": ${toJson(pendingContact.id.bytes)},
"state": "failed"
}
"""
assertJsonEquals(json, event.output())
}
@Test
fun testOutputPendingContactRemovedEvent() {
val event = PendingContactRemovedEvent(pendingContact.id)
val json = """
{
"pendingContactId": ${toJson(pendingContact.id.bytes)}
}
"""
assertJsonEquals(json, event.output())
}
} }

View File

@@ -23,7 +23,6 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory
import org.briarproject.briar.api.messaging.PrivateMessageHeader import org.briarproject.briar.api.messaging.PrivateMessageHeader
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent
import org.briarproject.briar.headless.ControllerTest 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.event.output
import org.briarproject.briar.headless.json.JsonDict import org.briarproject.briar.headless.json.JsonDict
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@@ -35,7 +34,6 @@ internal class MessagingControllerImplTest : ControllerTest() {
private val messagingManager = mockk<MessagingManager>() private val messagingManager = mockk<MessagingManager>()
private val conversationManager = mockk<ConversationManager>() private val conversationManager = mockk<ConversationManager>()
private val privateMessageFactory = mockk<PrivateMessageFactory>() private val privateMessageFactory = mockk<PrivateMessageFactory>()
private val webSocketController = mockk<WebSocketController>()
private val dbExecutor = ImmediateExecutor() private val dbExecutor = ImmediateExecutor()
private val controller = MessagingControllerImpl( private val controller = MessagingControllerImpl(

View File

@@ -3,7 +3,7 @@ dependencyVerification {
'com.fasterxml.jackson.core:jackson-annotations:2.9.0:jackson-annotations-2.9.0.jar:45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a', 'com.fasterxml.jackson.core:jackson-annotations:2.9.0:jackson-annotations-2.9.0.jar:45d32ac61ef8a744b464c54c2b3414be571016dd46bfc2bec226761cf7ae457a',
'com.fasterxml.jackson.core:jackson-core:2.9.8:jackson-core-2.9.8.jar:d934dab0bd48994eeea2c1b493cb547158a338a80b58c4fbc8e85fb0905e105f', 'com.fasterxml.jackson.core:jackson-core:2.9.8:jackson-core-2.9.8.jar:d934dab0bd48994eeea2c1b493cb547158a338a80b58c4fbc8e85fb0905e105f',
'com.fasterxml.jackson.core:jackson-databind:2.9.8:jackson-databind-2.9.8.jar:2351c3eba73a545db9079f5d6d768347ad72666537362c8220fe3e950a55a864', 'com.fasterxml.jackson.core:jackson-databind:2.9.8:jackson-databind-2.9.8.jar:2351c3eba73a545db9079f5d6d768347ad72666537362c8220fe3e950a55a864',
'com.github.ajalt:clikt:1.6.0:clikt-1.6.0.jar:ebab34d5a60817bb7d471a67cd1740b91a5d99e224660bddbcf32bac1651a575', 'com.github.ajalt:clikt:2.0.0:clikt-2.0.0.jar:c247adb96337e0799bf6d84f4c494df9d8f1e46e9157eacaf438d03323ee9475',
'com.google.code.findbugs:jsr305:1.3.9:jsr305-1.3.9.jar:905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed', 'com.google.code.findbugs:jsr305:1.3.9:jsr305-1.3.9.jar:905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed',
'com.google.dagger:dagger-compiler:2.22.1:dagger-compiler-2.22.1.jar:e5f28302cbe70a79d3620cddebfb8ec0736814f3980ffe1e673bfe3342f507d3', 'com.google.dagger:dagger-compiler:2.22.1:dagger-compiler-2.22.1.jar:e5f28302cbe70a79d3620cddebfb8ec0736814f3980ffe1e673bfe3342f507d3',
'com.google.dagger:dagger-producers:2.22.1:dagger-producers-2.22.1.jar:f834a0082014213a68ff06a0f048d750178d02196c58b0b15beb367d32b97e35', 'com.google.dagger:dagger-producers:2.22.1:dagger-producers-2.22.1.jar:f834a0082014213a68ff06a0f048d750178d02196c58b0b15beb367d32b97e35',
@@ -16,20 +16,20 @@ dependencyVerification {
'com.google.j2objc:j2objc-annotations:1.1:j2objc-annotations-1.1.jar:2994a7eb78f2710bd3d3bfb639b2c94e219cedac0d4d084d516e78c16dddecf6', 'com.google.j2objc:j2objc-annotations:1.1:j2objc-annotations-1.1.jar:2994a7eb78f2710bd3d3bfb639b2c94e219cedac0d4d084d516e78c16dddecf6',
'com.squareup:javapoet:1.11.1:javapoet-1.11.1.jar:9cbf2107be499ec6e95afd36b58e3ca122a24166cdd375732e51267d64058e90', 'com.squareup:javapoet:1.11.1:javapoet-1.11.1.jar:9cbf2107be499ec6e95afd36b58e3ca122a24166cdd375732e51267d64058e90',
'com.vaadin.external.google:android-json:0.0.20131108.vaadin1:android-json-0.0.20131108.vaadin1.jar:dfb7bae2f404cfe0b72b4d23944698cb716b7665171812a0a4d0f5926c0fac79', 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1:android-json-0.0.20131108.vaadin1.jar:dfb7bae2f404cfe0b72b4d23944698cb716b7665171812a0a4d0f5926c0fac79',
'io.javalin:javalin:2.7.0:javalin-2.7.0.jar:0f345ea86419813b2ba45a6d64d284f15099ced9a6ce51cae815e4caec724429', 'io.javalin:javalin:2.8.0:javalin-2.8.0.jar:1f2f8e60ba06b2d65058a4ca430fe74ba74c27c93b35c96a9c883bd960d6fb3f',
'io.mockk:mockk-agent-api:1.9.2:mockk-agent-api-1.9.2.jar:396d56cdba086c1bf01d4402591bb7b8fe46de75110c627eb88d2cd3e58bf6f5', 'io.mockk:mockk-agent-api:1.9.3:mockk-agent-api-1.9.3.jar:90b9b54158ad31aafa414cb7889bd5a9b70b23e990c5a72eb0c17c3322e6d12d',
'io.mockk:mockk-agent-common:1.9.2:mockk-agent-common-1.9.2.jar:ac8ab7a568df79ec80449b05c633458397acf40e8ae5db58cd3966d463509ebc', 'io.mockk:mockk-agent-common:1.9.3:mockk-agent-common-1.9.3.jar:a9ddd89f1e1393aa4b7e99d0032b961088bb8d51e48ff188ada3d1fa05696c88',
'io.mockk:mockk-agent-jvm:1.9.2:mockk-agent-jvm-1.9.2.jar:acf0336dc1802cf70450b594adc8f61dce2a6942e8162789dab60f1b54256ae1', 'io.mockk:mockk-agent-jvm:1.9.3:mockk-agent-jvm-1.9.3.jar:4e0661778c531d2849d9636f7896bbba314307fb45b47a0107f6a7ad31d1d531',
'io.mockk:mockk-common:1.9.2:mockk-common-1.9.2.jar:0f936f82427b0c7822cae8e303cdbd8ceee6204bed80eab3f18cf00f4b9b82a3', 'io.mockk:mockk-common:1.9.3:mockk-common-1.9.3.jar:05b6d77650171b13194dd0edcc36656897d04267e85e9e89c4ec187bdaaa6a3d',
'io.mockk:mockk-dsl-jvm:1.9.2:mockk-dsl-jvm-1.9.2.jar:6f7a3093f05876b24b26db3b6d6a568e0c20253489a588dbc9c67b43eb838ad0', 'io.mockk:mockk-dsl-jvm:1.9.3:mockk-dsl-jvm-1.9.3.jar:86c5c158640d244d19b29e894827e9d8c27741b4e13ed2ed3bb54b7a4ee4220f',
'io.mockk:mockk-dsl:1.9.2:mockk-dsl-1.9.2.jar:d681ad3a7063a2c7fb8f0164b8eb2fd0085fd9fb77af1a4f189039e73f2ec3c4', 'io.mockk:mockk-dsl:1.9.3:mockk-dsl-1.9.3.jar:1ccb814a192a5e4d2c59369ddc2499e8417f49ec9834e4f3dc4619877fd6069a',
'io.mockk:mockk:1.9.2:mockk-1.9.2.jar:96ce20b64c6d05218fed0b99e93b9b49c2c3f40dd96273b53f2e448a598b1c57', 'io.mockk:mockk:1.9.3:mockk-1.9.3.jar:875ec9f02fa42231510cade8c677b8598d9a0f5687b5cb25a1f188c1c41ef332',
'javax.annotation:jsr250-api:1.0:jsr250-api-1.0.jar:a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f', 'javax.annotation:jsr250-api:1.0:jsr250-api-1.0.jar:a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f',
'javax.inject:javax.inject:1:javax.inject-1.jar:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', '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', 'javax.servlet:javax.servlet-api:3.1.0:javax.servlet-api-3.1.0.jar:af456b2dd41c4e82cf54f3e743bc678973d9fe35bd4d3071fa05c7e5333b8482',
'khttp:khttp:0.1.0:khttp-0.1.0.jar:48ab3bd22e461f2c2e74e3446d8f9568e24aab157f61fdc85ded6c0bfbe9a926', 'khttp:khttp:0.1.0:khttp-0.1.0.jar:48ab3bd22e461f2c2e74e3446d8f9568e24aab157f61fdc85ded6c0bfbe9a926',
'net.bytebuddy:byte-buddy-agent:1.9.3:byte-buddy-agent-1.9.3.jar:547288e013a9d1f4a4ce2ab84c24e3edda6e433c7fa6b2c3c3613932671b05b1', 'net.bytebuddy:byte-buddy-agent:1.9.10:byte-buddy-agent-1.9.10.jar:8ed739d29132103250d307d2e8e3c95f07588ef0543ab11d2881d00768a5e182',
'net.bytebuddy:byte-buddy:1.9.3:byte-buddy-1.9.3.jar:a27350be602caea67a33d31281496c84c69b5ab34ddc228e9ff2253fc8f9cd31', 'net.bytebuddy:byte-buddy:1.9.10:byte-buddy-1.9.10.jar:2936debc4d7b6c534848d361412e2d0f8bd06f7f27a6f4e728a20e97648d2bf3',
'org.apiguardian:apiguardian-api:1.0.0:apiguardian-api-1.0.0.jar:1f58b77470d8d147a0538d515347dd322f49a83b9e884b8970051160464b65b3', 'org.apiguardian:apiguardian-api:1.0.0:apiguardian-api-1.0.0.jar:1f58b77470d8d147a0538d515347dd322f49a83b9e884b8970051160464b65b3',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d', 'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.codehaus.mojo:animal-sniffer-annotations:1.14:animal-sniffer-annotations-1.14.jar:2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d', 'org.codehaus.mojo:animal-sniffer-annotations:1.14:animal-sniffer-annotations-1.14.jar:2068320bd6bad744c3673ab048f67e30bef8f518996fa380033556600669905d',
@@ -48,37 +48,35 @@ dependencyVerification {
'org.eclipse.jetty:jetty-webapp:9.4.15.v20190215:jetty-webapp-9.4.15.v20190215.jar:81b56aa7c29513654827adc48e786f121b54183791c132255195b9a45d83a0f3', 'org.eclipse.jetty:jetty-webapp:9.4.15.v20190215:jetty-webapp-9.4.15.v20190215.jar:81b56aa7c29513654827adc48e786f121b54183791c132255195b9a45d83a0f3',
'org.eclipse.jetty:jetty-xml:9.4.15.v20190215:jetty-xml-9.4.15.v20190215.jar:c6d97a70572d5400e9ff3b7e32d4a4fd1c61319cbf997655a608064a75466082', 'org.eclipse.jetty:jetty-xml:9.4.15.v20190215:jetty-xml-9.4.15.v20190215.jar:c6d97a70572d5400e9ff3b7e32d4a4fd1c61319cbf997655a608064a75466082',
'org.jetbrains.intellij.deps:trove4j:1.0.20181211:trove4j-1.0.20181211.jar:affb7c85a3c87bdcf69ff1dbb84de11f63dc931293934bc08cd7ab18de083601', 'org.jetbrains.intellij.deps:trove4j:1.0.20181211:trove4j-1.0.20181211.jar:affb7c85a3c87bdcf69ff1dbb84de11f63dc931293934bc08cd7ab18de083601',
'org.jetbrains.kotlin:kotlin-android-extensions:1.3.21:kotlin-android-extensions-1.3.21.jar:2b0462ac3e4b36dffdb3bfa6173cb41b0e24e25a7d7eee1012471f1d27aea2dd', 'org.jetbrains.kotlin:kotlin-android-extensions:1.3.31:kotlin-android-extensions-1.3.31.jar:2f849616dcf5a5aa372e6c11ccd196607f0c3d42dd0a9be6d49ee3732ca050ba',
'org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.3.21:kotlin-annotation-processing-gradle-1.3.21.jar:faf880315d4fd6a666cc17aa5e9608c7468c70a279b49ccca67dba2a54adf692', 'org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.3.31:kotlin-annotation-processing-gradle-1.3.31.jar:29a5fb59416226e2326f9fcb3ad0974915a424eec9125449981e1b9bbd9b79d6',
'org.jetbrains.kotlin:kotlin-build-common:1.3.21:kotlin-build-common-1.3.21.jar:f4d8d08c6f5966d9d517ced60c5224c7edca2d811ea0a702bd7199a00dd4fa25', 'org.jetbrains.kotlin:kotlin-build-common:1.3.31:kotlin-build-common-1.3.31.jar:a37bace5fce25dade884ea75972fcf2a67d6f1326bf300eca27d052423773267',
'org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.21:kotlin-compiler-embeddable-1.3.21.jar:afaaedc324fbf6394d9f39544efcc93cfc59f8a5aa1a1a5c71d61e2483666c6a', 'org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.31:kotlin-compiler-embeddable-1.3.31.jar:b7918cbce747683905486ae54e664fe5d5db60e8ed1cbfebc00c79912b9aaffd',
'org.jetbrains.kotlin:kotlin-compiler-runner:1.3.21:kotlin-compiler-runner-1.3.21.jar:73e7088a074f9c517cd4bb2a8611834168459661c832136cf3628ccd5994cc3b', 'org.jetbrains.kotlin:kotlin-compiler-runner:1.3.31:kotlin-compiler-runner-1.3.31.jar:f8ab33e2ec54a1c62a189c0cab04fbadb58dfd1bdda6a8ade0849a7a9a598b7c',
'org.jetbrains.kotlin:kotlin-daemon-client:1.3.21:kotlin-daemon-client-1.3.21.jar:b3ecce11ec7b311ee0d1ccc65e811f3748f328010765e86cbdb29b2b70f73f1c', 'org.jetbrains.kotlin:kotlin-daemon-client:1.3.31:kotlin-daemon-client-1.3.31.jar:f658006ac301cae33e2a6cb1afd3cc41e82d98b12876de8fbe70a202434162de',
'org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.3.21:kotlin-gradle-plugin-api-1.3.21.jar:ed0ab11437310cd409657c5e5f8a6bf589af0a8348577cd600f54601fc97c369', 'org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.3.31:kotlin-gradle-plugin-api-1.3.31.jar:e40152d09ec45eb9fd4c0a8340de46793ae3beeb0f70f8ab15dc0097767fc61c',
'org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.3.21:kotlin-gradle-plugin-model-1.3.21.jar:fbade67a2a3fb234e2d4c1b8f07b2af6c096993f34ed732fe6fadaf696bc208a', 'org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.3.31:kotlin-gradle-plugin-model-1.3.31.jar:9bbe7b3afebb43e81ef4e6a3202eb86d51dee34ddb305090d5cf0f2861ce87be',
'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21:kotlin-gradle-plugin-1.3.21.jar:7858c58f4c678a8416520f4c094282a481981cfe702d23121118c9c7e9ad2326', 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.31:kotlin-gradle-plugin-1.3.31.jar:307ced92080a1d7a887fd7f71eef7b297b514a205ecf947220bd7ce8391a5594',
'org.jetbrains.kotlin:kotlin-native-utils:1.3.21:kotlin-native-utils-1.3.21.jar:406010a39f4c8cdd2351cc1110b98ed804c0aa810cb6106e7b9f4f2bcc21cd47', 'org.jetbrains.kotlin:kotlin-native-utils:1.3.31:kotlin-native-utils-1.3.31.jar:00af02020516eed7942ace3811cacd9fa3b1de2b66c6498e17dbe3a3e9bacce1',
'org.jetbrains.kotlin:kotlin-reflect:1.3.0:kotlin-reflect-1.3.0.jar:f3231ac1c612fe72de6ffcc4f0b4c5d85ad1ad4c808fb01a1981eab1ee1202c3', 'org.jetbrains.kotlin:kotlin-reflect:1.3.0:kotlin-reflect-1.3.0.jar:f3231ac1c612fe72de6ffcc4f0b4c5d85ad1ad4c808fb01a1981eab1ee1202c3',
'org.jetbrains.kotlin:kotlin-reflect:1.3.21:kotlin-reflect-1.3.21.jar:a3065c822633191e0a3e3ee12a29bec234fc4b2864a6bb87ef48cce3e9e0c26a', 'org.jetbrains.kotlin:kotlin-reflect:1.3.31:kotlin-reflect-1.3.31.jar:a0172daf57e511e8e0df9251b508db8aa6b885cdf0c5849addc9b840db4814f0',
'org.jetbrains.kotlin:kotlin-script-runtime:1.3.21:kotlin-script-runtime-1.3.21.jar:2e25babc8dcd224b9c479e2c16ce7b4c50407d25f18d60d1fd262f78c2b474cb', 'org.jetbrains.kotlin:kotlin-script-runtime:1.3.31:kotlin-script-runtime-1.3.31.jar:633692186b292292e41ea60d5170e811845b78aba88e20260ba70f7ce3a3ef32',
'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.3.21:kotlin-scripting-compiler-embeddable-1.3.21.jar:f4e6f9fd384d42167e9b89f985ee4a48a0676bfe705b2e2f9d13e1591d4b7c0b', 'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.3.31:kotlin-scripting-compiler-embeddable-1.3.31.jar:4dff2f683f8ceee0e834aeb0ca2686774da6c010ad1faf671dcaf73f071de954',
'org.jetbrains.kotlin:kotlin-stdlib-common:1.3.10:kotlin-stdlib-common-1.3.10.jar:7ebf12fdadc5fe80f7ed4dbeffb16618cee1c23cbff0b0489a254174500acc68', 'org.jetbrains.kotlin:kotlin-stdlib-common:1.3.31:kotlin-stdlib-common-1.3.31.jar:d6e9c54c1e6c4df21be9395de558665544c6bdc8f8076ea7518f089f82cd34fc',
'org.jetbrains.kotlin:kotlin-stdlib-common:1.3.21:kotlin-stdlib-common-1.3.21.jar:cea61f7b611895e64f58569a9757fc0ab0d582f107211e1930e0ce2a0add52a7', 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.31:kotlin-stdlib-jdk7-1.3.31.jar:dbf77e6a5626d941450fdc59cbfe24165858403c12789749a2497265269859a3',
'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.71:kotlin-stdlib-jdk7-1.2.71.jar:b136bd61b240e07d4d92ce00d3bd1dbf584400a7bf5f220c2f3cd22446858082', 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.31:kotlin-stdlib-jdk8-1.3.31.jar:ad6acd219b468a532ac3b3c5aacbfd5db02d0ffcf967e2113e4677e2429490f6',
'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71:kotlin-stdlib-jdk8-1.2.71.jar:ac3c8abf47790b64b4f7e2509a53f0c145e061ac1612a597520535d199946ea9', 'org.jetbrains.kotlin:kotlin-stdlib:1.3.31:kotlin-stdlib-1.3.31.jar:f38c84326543e66ed4895b20fb3ea0fca527fd5a040e1f49d0946ecf3d2b3b23',
'org.jetbrains.kotlin:kotlin-stdlib:1.3.10:kotlin-stdlib-1.3.10.jar:9b9650550fac559f7db64d988123399ea3da7cb776bfb13b9a3ed818eef26969',
'org.jetbrains.kotlin:kotlin-stdlib:1.3.21:kotlin-stdlib-1.3.21.jar:38ba2370d9f06f50433e06b2ca775b94473c2e2785f410926079ab793c72b034',
'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478', 'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478',
'org.json:json:20150729:json-20150729.jar:38c21b9c3d6d24919cd15d027d20afab0a019ac9205f7ed9083b32bdd42a2353', 'org.json:json:20150729:json-20150729.jar:38c21b9c3d6d24919cd15d027d20afab0a019ac9205f7ed9083b32bdd42a2353',
'org.junit.jupiter:junit-jupiter-api:5.3.1:junit-jupiter-api-5.3.1.jar:7923e21f030a9964d70a0e48007ca873280c66ddf0f0620b2d969852c23d5653', 'org.junit.jupiter:junit-jupiter-api:5.4.2:junit-jupiter-api-5.4.2.jar:cdfb355fee661633f15f2763b8c2029c2e1958585b97b9162d38a36b1754dc3e',
'org.junit.jupiter:junit-jupiter-engine:5.3.1:junit-jupiter-engine-5.3.1.jar:04f4354548a30827e126bdf6fcbe3640789ad8335a6f3f0762bf7f9f74e51fbf', 'org.junit.jupiter:junit-jupiter-engine:5.4.2:junit-jupiter-engine-5.4.2.jar:42aead7c5c1b74e0ef775c374a9fc07c771fd61a3621e66df1793dba14e534fd',
'org.junit.jupiter:junit-jupiter-params:5.3.1:junit-jupiter-params-5.3.1.jar:72fe344712d4cd88dd0cb4bfa304322d512d2cb27173ed64cb5036a573d29f4c', 'org.junit.jupiter:junit-jupiter-params:5.4.2:junit-jupiter-params-5.4.2.jar:13f89bca59fb6931a0ca9e3f4dc74e1a3054e0c63863e091a5df4855605ae4ce',
'org.junit.platform:junit-platform-commons:1.3.1:junit-platform-commons-1.3.1.jar:457d8e1c0c80d1e320a792ec35e7c180694ba05182d7ecf7dabdb4e5a8a12fe2', 'org.junit.platform:junit-platform-commons:1.4.2:junit-platform-commons-1.4.2.jar:104bfa65b30ceb425a6de19d66b976caf38443ff5978ae931c103fa0f99d04ce',
'org.junit.platform:junit-platform-engine:1.3.1:junit-platform-engine-1.3.1.jar:303d0546c3e950cc3beaca00dfcbf2632d4eca530e4e446391bf193cbc2a71a3', 'org.junit.platform:junit-platform-engine:1.4.2:junit-platform-engine-1.4.2.jar:7edb2ad879a338a84dbb09202b1399640ec0cacc5a95168539a9a74b5a2302e1',
'org.objenesis:objenesis:2.6:objenesis-2.6.jar:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d', 'org.objenesis:objenesis:3.0.1:objenesis-3.0.1.jar:7a8ff780b9ff48415d7c705f60030b0acaa616e7f823c98eede3b63508d4e984',
'org.opentest4j:opentest4j:1.1.1:opentest4j-1.1.1.jar:f106351abd941110226745ed103c85863b3f04e9fa82ddea1084639ae0c5336c', 'org.opentest4j:opentest4j:1.1.1:opentest4j-1.1.1.jar:f106351abd941110226745ed103c85863b3f04e9fa82ddea1084639ae0c5336c',
'org.skyscreamer:jsonassert:1.5.0:jsonassert-1.5.0.jar:a310bc79c3f4744e2b2e993702fcebaf3696fec0063643ffdc6b49a8fb03ef39', '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-api:1.7.26:slf4j-api-1.7.26.jar:6d9e5b86cfd1dd44c676899285b5bb4fa0d371cf583e8164f9c8a0366553242b',
'org.slf4j:slf4j-simple:1.7.25:slf4j-simple-1.7.25.jar:0966e86fffa5be52d3d9e7b89dd674d98a03eed0a454fbaf7c1bd9493bd9d874', 'org.slf4j:slf4j-simple:1.7.26:slf4j-simple-1.7.26.jar:4b8ed75e2273850bf4eeb411ae5de5e0c0a44da59a96ca68d284749a6a373678',
] ]
} }