diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java index 004977060..3a1d3f80a 100644 --- a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java +++ b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java @@ -5,6 +5,7 @@ import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.PendingContact; 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.AgreementPublicKey; import org.briarproject.bramble.api.crypto.PrivateKey; @@ -36,7 +37,6 @@ import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; 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_SIGNATURE_PUBLIC_KEY_BYTES; import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION; @@ -181,8 +181,10 @@ public class TestUtils { PendingContactId id = new PendingContactId(getRandomId()); PublicKey publicKey = getAgreementPublicKey(); String alias = getRandomString(nameLength); - return new PendingContact(id, publicKey, alias, WAITING_FOR_CONNECTION, - timestamp); + int stateIndex = + random.nextInt(PendingContactState.values().length - 1); + PendingContactState state = PendingContactState.values()[stateIndex]; + return new PendingContact(id, publicKey, alias, state, timestamp); } public static ContactId getContactId() { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java index d72fd994f..37939e6d0 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/DatabaseComponentImpl.java @@ -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.ContactRemovedEvent; 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.PublicKey; import org.briarproject.bramble.api.crypto.SecretKey; @@ -293,6 +295,8 @@ class DatabaseComponentImpl implements DatabaseComponent { if (db.containsPendingContact(txn, p.getId())) throw new PendingContactExistsException(); db.addPendingContact(txn, p); + transaction.attach(new PendingContactStateChangedEvent(p.getId(), + p.getState())); } @Override @@ -892,6 +896,7 @@ class DatabaseComponentImpl implements DatabaseComponent { if (!db.containsPendingContact(txn, p)) throw new NoSuchPendingContactException(); db.removePendingContact(txn, p); + transaction.attach(new PendingContactRemovedEvent(p)); } @Override diff --git a/briar-core/build.gradle b/briar-core/build.gradle index c37cedb75..cd3fee934 100644 --- a/briar-core/build.gradle +++ b/briar-core/build.gradle @@ -30,3 +30,15 @@ dependencies { 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 +} diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java index 314040cfe..8f6f97226 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java @@ -1,13 +1,19 @@ 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.identity.Author; import org.briarproject.bramble.api.identity.AuthorFactory; import org.briarproject.bramble.api.identity.LocalAuthor; 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.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.util.StringUtils.getRandomString; import static org.junit.Assert.assertEquals; @@ -40,4 +46,13 @@ public class BriarTestUtils { 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(); + } + } diff --git a/briar-headless/README.md b/briar-headless/README.md index 29aa1cec5..02a7d40d2 100644 --- a/briar-headless/README.md +++ b/briar-headless/README.md @@ -65,16 +65,87 @@ Returns a JSON array of contacts: "publicKey": "BDu6h1S02bF4W6rgoZfZ6BMjTj/9S9hNN7EQoV05qUo=" }, "contactId": 1, + "alias" : "A local nickname", + "handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=", "verified": true } ``` ### 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 -and then move its database (and key files) to the headless peer. +`GET /v1/contacts/add/link` + +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 @@ -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 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" +} +``` diff --git a/briar-headless/build.gradle b/briar-headless/build.gradle index 0c6eee26e..c9b463af1 100644 --- a/briar-headless/build.gradle +++ b/briar-headless/build.gradle @@ -1,8 +1,8 @@ plugins { id 'java' id 'idea' - id 'org.jetbrains.kotlin.jvm' version '1.3.21' - id 'org.jetbrains.kotlin.kapt' version '1.3.21' + id 'org.jetbrains.kotlin.jvm' version '1.3.31' + id 'org.jetbrains.kotlin.kapt' version '1.3.31' id 'witness' } apply from: 'witness.gradle' @@ -14,25 +14,28 @@ dependencies { implementation project(path: ':briar-core', configuration: 'default') implementation project(path: ':bramble-java', configuration: 'default') - implementation 'io.javalin:javalin:2.7.0' - implementation 'org.slf4j:slf4j-simple:1.7.25' + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.31' + 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.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-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-params:$junitVersion" testRuntime "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testImplementation "io.mockk:mockk:1.9.2" - testImplementation "org.skyscreamer:jsonassert:1.5.0" + testImplementation 'io.mockk:mockk:1.9.3' + testImplementation 'org.skyscreamer:jsonassert:1.5.0' testImplementation 'khttp:khttp:0.1.0' - kaptTest 'com.google.dagger:dagger-compiler:2.22.1' + kaptTest "com.google.dagger:dagger-compiler:$daggerVersion" } jar { diff --git a/briar-headless/src/main/java/org/briarproject/bramble/identity/OutputAuthor.kt b/briar-headless/src/main/java/org/briarproject/bramble/identity/OutputAuthor.kt index 3b6617582..cbdb0387e 100644 --- a/briar-headless/src/main/java/org/briarproject/bramble/identity/OutputAuthor.kt +++ b/briar-headless/src/main/java/org/briarproject/bramble/identity/OutputAuthor.kt @@ -8,7 +8,7 @@ fun Author.output() = JsonDict( "formatVersion" to formatVersion, "id" to id.bytes, "name" to name, - "publicKey" to publicKey + "publicKey" to publicKey.encoded ) fun AuthorInfo.Status.output() = name.toLowerCase() diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt index e16f3eb4c..bb1f49cae 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt @@ -64,6 +64,16 @@ constructor( path("/v1") { path("/contacts") { 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") { delete { ctx -> contactController.delete(ctx) } } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactController.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactController.kt index d7a0bfbc4..120015f02 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactController.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactController.kt @@ -5,6 +5,10 @@ import io.javalin.Context interface ContactController { 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 } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt index d09c44dbf..657649260 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt @@ -1,19 +1,58 @@ package org.briarproject.briar.headless.contact +import com.fasterxml.jackson.databind.ObjectMapper +import io.javalin.BadRequestResponse import io.javalin.Context import io.javalin.NotFoundResponse 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.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.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.inject.Inject 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 @Singleton internal class ContactControllerImpl @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 { val contacts = contactManager.contacts.map { contact -> @@ -22,6 +61,48 @@ constructor(private val contactManager: ContactManager) : ContactController { 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 { val contactId = ctx.getContactIdFromPathParam() try { diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/HeadlessContactModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/HeadlessContactModule.kt index bf91804b3..032af9778 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/HeadlessContactModule.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/HeadlessContactModule.kt @@ -2,6 +2,7 @@ package org.briarproject.briar.headless.contact import dagger.Module import dagger.Provides +import org.briarproject.bramble.api.event.EventBus import javax.inject.Singleton @Module @@ -9,7 +10,11 @@ class HeadlessContactModule { @Provides @Singleton - internal fun provideContactController(contactController: ContactControllerImpl): ContactController { + internal fun provideContactController( + eventBus: EventBus, + contactController: ContactControllerImpl + ): ContactController { + eventBus.addListener(contactController) return contactController } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt index d0ec5898c..7f5f35f54 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt @@ -1,6 +1,7 @@ package org.briarproject.briar.headless.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.briar.headless.json.JsonDict @@ -8,4 +9,11 @@ internal fun Contact.output() = JsonDict( "contactId" to id.int, "author" to author.output(), "verified" to isVerified -) \ No newline at end of file +).apply { + alias?.let { put("alias", it) } + handshakePublicKey?.let { put("handshakePublicKey", it.encoded) } +} + +internal fun ContactAddedRemotelyEvent.output() = JsonDict( + "contact" to contact.output() +) diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt new file mode 100644 index 000000000..1988bb342 --- /dev/null +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt @@ -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 +) diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/BriarHeadlessTestApp.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/BriarHeadlessTestApp.kt index f349de62b..3bf488150 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/BriarHeadlessTestApp.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/BriarHeadlessTestApp.kt @@ -4,6 +4,7 @@ import dagger.Component import org.briarproject.bramble.BrambleCoreEagerSingletons import org.briarproject.bramble.BrambleCoreModule import org.briarproject.bramble.account.AccountModule +import org.briarproject.bramble.api.crypto.CryptoComponent import org.briarproject.bramble.event.DefaultEventExecutorModule import org.briarproject.bramble.test.TestSecureRandomModule import org.briarproject.briar.BriarCoreEagerSingletons @@ -25,5 +26,7 @@ import javax.inject.Singleton internal interface BriarHeadlessTestApp : BrambleCoreEagerSingletons, BriarCoreEagerSingletons { fun getRouter(): Router + fun getCryptoComponent(): CryptoComponent + fun getTestDataCreator(): TestDataCreator } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt index d9ffe4872..b461cb5f8 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt @@ -14,6 +14,7 @@ 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.briarproject.briar.headless.event.WebSocketController import org.skyscreamer.jsonassert.JSONAssert.assertEquals import org.skyscreamer.jsonassert.JSONCompareMode.STRICT import javax.servlet.http.HttpServletRequest @@ -26,6 +27,8 @@ abstract class ControllerTest { protected val clock = mockk() protected val ctx = mockk() + protected val webSocketController = mockk() + private val request = mockk(relaxed = true) private val response = mockk(relaxed = true) private val outputCtx = ContextUtil.init(request, response) diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/IntegrationTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/IntegrationTest.kt index 0884f34ff..9f53cd275 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/IntegrationTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/IntegrationTest.kt @@ -4,6 +4,7 @@ import io.javalin.Javalin import io.javalin.core.util.Header.AUTHORIZATION import khttp.responses.Response import org.briarproject.bramble.BrambleCoreModule +import org.briarproject.bramble.api.crypto.CryptoComponent import org.briarproject.briar.BriarCoreModule import org.briarproject.briar.api.test.TestDataCreator import org.junit.jupiter.api.AfterAll @@ -22,6 +23,7 @@ abstract class IntegrationTest { private val dataDir = File("tmp") protected lateinit var api: Javalin + protected lateinit var crypto: CryptoComponent protected lateinit var testDataCreator: TestDataCreator private lateinit var router: Router @@ -33,6 +35,7 @@ abstract class IntegrationTest { BrambleCoreModule.initEagerSingletons(app) BriarCoreModule.initEagerSingletons(app) router = app.getRouter() + crypto = app.getCryptoComponent() testDataCreator = app.getTestDataCreator() api = router.start(token, port, false) @@ -52,10 +55,22 @@ abstract class IntegrationTest { 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 { 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 { return khttp.delete(url, getAuthTokenHeader("wrongToken")) } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerIntegrationTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerIntegrationTest.kt index f00024620..bd63c1193 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerIntegrationTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerIntegrationTest.kt @@ -1,8 +1,11 @@ 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.url +import org.briarproject.briar.test.BriarTestUtils.getRealHandshakeLink import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class ContactControllerIntegrationTest: IntegrationTest() { @@ -33,6 +36,75 @@ class ContactControllerIntegrationTest: IntegrationTest() { 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 fun `deleting contact need authentication token`() { val response = deleteWithWrongToken("$url/contacts/1") diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt index 34f14b393..05128f28a 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt @@ -1,21 +1,38 @@ package org.briarproject.briar.headless.contact +import io.javalin.BadRequestResponse import io.javalin.NotFoundResponse import io.javalin.json.JavalinJson.toJson import io.mockk.Runs import io.mockk.every import io.mockk.just +import io.mockk.runs import org.briarproject.bramble.api.contact.Contact 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.NoSuchPendingContactException +import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH 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.json.JsonDict +import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test internal class ContactControllerTest : ControllerTest() { - private val controller = ContactControllerImpl(contactManager) + private val pendingContact = getPendingContact() + + private val controller = + ContactControllerImpl(contactManager, objectMapper, webSocketController) @Test fun testEmptyContactList() { @@ -31,6 +48,131 @@ internal class ContactControllerTest : ControllerTest() { 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 fun testDelete() { 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 fun testOutputContact() { + assertNotNull(contact.handshakePublicKey) val json = """ { "contactId": ${contact.id.int}, "author": ${toJson(author.output())}, + "alias" : "${contact.alias}", + "handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)}, "verified": ${contact.isVerified} } """ @@ -74,10 +261,57 @@ internal class ContactControllerTest : ControllerTest() { "formatVersion": 1, "id": ${toJson(author.id.bytes)}, "name": "${author.name}", - "publicKey": ${toJson(author.publicKey)} + "publicKey": ${toJson(author.publicKey.encoded)} } """ 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()) + } + } diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt index c438a768b..d5ab17dc9 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/messaging/MessagingControllerImplTest.kt @@ -23,7 +23,6 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory 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.event.WebSocketController import org.briarproject.briar.headless.event.output import org.briarproject.briar.headless.json.JsonDict import org.junit.jupiter.api.Assertions.assertEquals @@ -35,7 +34,6 @@ internal class MessagingControllerImplTest : ControllerTest() { private val messagingManager = mockk() private val conversationManager = mockk() private val privateMessageFactory = mockk() - private val webSocketController = mockk() private val dbExecutor = ImmediateExecutor() private val controller = MessagingControllerImpl( diff --git a/briar-headless/witness.gradle b/briar-headless/witness.gradle index 2a393aa72..347be7028 100644 --- a/briar-headless/witness.gradle +++ b/briar-headless/witness.gradle @@ -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-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.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.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', @@ -16,20 +16,20 @@ dependencyVerification { '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.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.mockk:mockk-agent-api:1.9.2:mockk-agent-api-1.9.2.jar:396d56cdba086c1bf01d4402591bb7b8fe46de75110c627eb88d2cd3e58bf6f5', - 'io.mockk:mockk-agent-common:1.9.2:mockk-agent-common-1.9.2.jar:ac8ab7a568df79ec80449b05c633458397acf40e8ae5db58cd3966d463509ebc', - 'io.mockk:mockk-agent-jvm:1.9.2:mockk-agent-jvm-1.9.2.jar:acf0336dc1802cf70450b594adc8f61dce2a6942e8162789dab60f1b54256ae1', - 'io.mockk:mockk-common:1.9.2:mockk-common-1.9.2.jar:0f936f82427b0c7822cae8e303cdbd8ceee6204bed80eab3f18cf00f4b9b82a3', - 'io.mockk:mockk-dsl-jvm:1.9.2:mockk-dsl-jvm-1.9.2.jar:6f7a3093f05876b24b26db3b6d6a568e0c20253489a588dbc9c67b43eb838ad0', - 'io.mockk:mockk-dsl:1.9.2:mockk-dsl-1.9.2.jar:d681ad3a7063a2c7fb8f0164b8eb2fd0085fd9fb77af1a4f189039e73f2ec3c4', - 'io.mockk:mockk:1.9.2:mockk-1.9.2.jar:96ce20b64c6d05218fed0b99e93b9b49c2c3f40dd96273b53f2e448a598b1c57', + 'io.javalin:javalin:2.8.0:javalin-2.8.0.jar:1f2f8e60ba06b2d65058a4ca430fe74ba74c27c93b35c96a9c883bd960d6fb3f', + 'io.mockk:mockk-agent-api:1.9.3:mockk-agent-api-1.9.3.jar:90b9b54158ad31aafa414cb7889bd5a9b70b23e990c5a72eb0c17c3322e6d12d', + 'io.mockk:mockk-agent-common:1.9.3:mockk-agent-common-1.9.3.jar:a9ddd89f1e1393aa4b7e99d0032b961088bb8d51e48ff188ada3d1fa05696c88', + 'io.mockk:mockk-agent-jvm:1.9.3:mockk-agent-jvm-1.9.3.jar:4e0661778c531d2849d9636f7896bbba314307fb45b47a0107f6a7ad31d1d531', + 'io.mockk:mockk-common:1.9.3:mockk-common-1.9.3.jar:05b6d77650171b13194dd0edcc36656897d04267e85e9e89c4ec187bdaaa6a3d', + 'io.mockk:mockk-dsl-jvm:1.9.3:mockk-dsl-jvm-1.9.3.jar:86c5c158640d244d19b29e894827e9d8c27741b4e13ed2ed3bb54b7a4ee4220f', + 'io.mockk:mockk-dsl:1.9.3:mockk-dsl-1.9.3.jar:1ccb814a192a5e4d2c59369ddc2499e8417f49ec9834e4f3dc4619877fd6069a', + '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.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', '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:1.9.3:byte-buddy-1.9.3.jar:a27350be602caea67a33d31281496c84c69b5ab34ddc228e9ff2253fc8f9cd31', + 'net.bytebuddy:byte-buddy-agent:1.9.10:byte-buddy-agent-1.9.10.jar:8ed739d29132103250d307d2e8e3c95f07588ef0543ab11d2881d00768a5e182', + '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.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', @@ -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-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.kotlin:kotlin-android-extensions:1.3.21:kotlin-android-extensions-1.3.21.jar:2b0462ac3e4b36dffdb3bfa6173cb41b0e24e25a7d7eee1012471f1d27aea2dd', - 'org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.3.21:kotlin-annotation-processing-gradle-1.3.21.jar:faf880315d4fd6a666cc17aa5e9608c7468c70a279b49ccca67dba2a54adf692', - 'org.jetbrains.kotlin:kotlin-build-common:1.3.21:kotlin-build-common-1.3.21.jar:f4d8d08c6f5966d9d517ced60c5224c7edca2d811ea0a702bd7199a00dd4fa25', - 'org.jetbrains.kotlin:kotlin-compiler-embeddable:1.3.21:kotlin-compiler-embeddable-1.3.21.jar:afaaedc324fbf6394d9f39544efcc93cfc59f8a5aa1a1a5c71d61e2483666c6a', - 'org.jetbrains.kotlin:kotlin-compiler-runner:1.3.21:kotlin-compiler-runner-1.3.21.jar:73e7088a074f9c517cd4bb2a8611834168459661c832136cf3628ccd5994cc3b', - 'org.jetbrains.kotlin:kotlin-daemon-client:1.3.21:kotlin-daemon-client-1.3.21.jar:b3ecce11ec7b311ee0d1ccc65e811f3748f328010765e86cbdb29b2b70f73f1c', - '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-model:1.3.21:kotlin-gradle-plugin-model-1.3.21.jar:fbade67a2a3fb234e2d4c1b8f07b2af6c096993f34ed732fe6fadaf696bc208a', - 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.21:kotlin-gradle-plugin-1.3.21.jar:7858c58f4c678a8416520f4c094282a481981cfe702d23121118c9c7e9ad2326', - 'org.jetbrains.kotlin:kotlin-native-utils:1.3.21:kotlin-native-utils-1.3.21.jar:406010a39f4c8cdd2351cc1110b98ed804c0aa810cb6106e7b9f4f2bcc21cd47', + '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.31:kotlin-annotation-processing-gradle-1.3.31.jar:29a5fb59416226e2326f9fcb3ad0974915a424eec9125449981e1b9bbd9b79d6', + '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.31:kotlin-compiler-embeddable-1.3.31.jar:b7918cbce747683905486ae54e664fe5d5db60e8ed1cbfebc00c79912b9aaffd', + '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.31:kotlin-daemon-client-1.3.31.jar:f658006ac301cae33e2a6cb1afd3cc41e82d98b12876de8fbe70a202434162de', + '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.31:kotlin-gradle-plugin-model-1.3.31.jar:9bbe7b3afebb43e81ef4e6a3202eb86d51dee34ddb305090d5cf0f2861ce87be', + '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.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.21:kotlin-reflect-1.3.21.jar:a3065c822633191e0a3e3ee12a29bec234fc4b2864a6bb87ef48cce3e9e0c26a', - 'org.jetbrains.kotlin:kotlin-script-runtime:1.3.21:kotlin-script-runtime-1.3.21.jar:2e25babc8dcd224b9c479e2c16ce7b4c50407d25f18d60d1fd262f78c2b474cb', - 'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.3.21:kotlin-scripting-compiler-embeddable-1.3.21.jar:f4e6f9fd384d42167e9b89f985ee4a48a0676bfe705b2e2f9d13e1591d4b7c0b', - '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.21:kotlin-stdlib-common-1.3.21.jar:cea61f7b611895e64f58569a9757fc0ab0d582f107211e1930e0ce2a0add52a7', - 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.71:kotlin-stdlib-jdk7-1.2.71.jar:b136bd61b240e07d4d92ce00d3bd1dbf584400a7bf5f220c2f3cd22446858082', - 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.71:kotlin-stdlib-jdk8-1.2.71.jar:ac3c8abf47790b64b4f7e2509a53f0c145e061ac1612a597520535d199946ea9', - '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.kotlin:kotlin-reflect:1.3.31:kotlin-reflect-1.3.31.jar:a0172daf57e511e8e0df9251b508db8aa6b885cdf0c5849addc9b840db4814f0', + '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.31:kotlin-scripting-compiler-embeddable-1.3.31.jar:4dff2f683f8ceee0e834aeb0ca2686774da6c010ad1faf671dcaf73f071de954', + 'org.jetbrains.kotlin:kotlin-stdlib-common:1.3.31:kotlin-stdlib-common-1.3.31.jar:d6e9c54c1e6c4df21be9395de558665544c6bdc8f8076ea7518f089f82cd34fc', + 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.31:kotlin-stdlib-jdk7-1.3.31.jar:dbf77e6a5626d941450fdc59cbfe24165858403c12789749a2497265269859a3', + 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.31:kotlin-stdlib-jdk8-1.3.31.jar:ad6acd219b468a532ac3b3c5aacbfd5db02d0ffcf967e2113e4677e2429490f6', + 'org.jetbrains.kotlin:kotlin-stdlib:1.3.31:kotlin-stdlib-1.3.31.jar:f38c84326543e66ed4895b20fb3ea0fca527fd5a040e1f49d0946ecf3d2b3b23', 'org.jetbrains:annotations:13.0:annotations-13.0.jar:ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478', '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-engine:5.3.1:junit-jupiter-engine-5.3.1.jar:04f4354548a30827e126bdf6fcbe3640789ad8335a6f3f0762bf7f9f74e51fbf', - 'org.junit.jupiter:junit-jupiter-params:5.3.1:junit-jupiter-params-5.3.1.jar:72fe344712d4cd88dd0cb4bfa304322d512d2cb27173ed64cb5036a573d29f4c', - 'org.junit.platform:junit-platform-commons:1.3.1:junit-platform-commons-1.3.1.jar:457d8e1c0c80d1e320a792ec35e7c180694ba05182d7ecf7dabdb4e5a8a12fe2', - 'org.junit.platform:junit-platform-engine:1.3.1:junit-platform-engine-1.3.1.jar:303d0546c3e950cc3beaca00dfcbf2632d4eca530e4e446391bf193cbc2a71a3', - 'org.objenesis:objenesis:2.6:objenesis-2.6.jar:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d', + 'org.junit.jupiter:junit-jupiter-api:5.4.2:junit-jupiter-api-5.4.2.jar:cdfb355fee661633f15f2763b8c2029c2e1958585b97b9162d38a36b1754dc3e', + 'org.junit.jupiter:junit-jupiter-engine:5.4.2:junit-jupiter-engine-5.4.2.jar:42aead7c5c1b74e0ef775c374a9fc07c771fd61a3621e66df1793dba14e534fd', + 'org.junit.jupiter:junit-jupiter-params:5.4.2:junit-jupiter-params-5.4.2.jar:13f89bca59fb6931a0ca9e3f4dc74e1a3054e0c63863e091a5df4855605ae4ce', + 'org.junit.platform:junit-platform-commons:1.4.2:junit-platform-commons-1.4.2.jar:104bfa65b30ceb425a6de19d66b976caf38443ff5978ae931c103fa0f99d04ce', + 'org.junit.platform:junit-platform-engine:1.4.2:junit-platform-engine-1.4.2.jar:7edb2ad879a338a84dbb09202b1399640ec0cacc5a95168539a9a74b5a2302e1', + '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.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', + 'org.slf4j:slf4j-api:1.7.26:slf4j-api-1.7.26.jar:6d9e5b86cfd1dd44c676899285b5bb4fa0d371cf583e8164f9c8a0366553242b', + 'org.slf4j:slf4j-simple:1.7.26:slf4j-simple-1.7.26.jar:4b8ed75e2273850bf4eeb411ae5de5e0c0a44da59a96ca68d284749a6a373678', ] }