mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
[headless] expose ContactManager methods for adding contacts remotely
This commit is contained in:
@@ -71,10 +71,79 @@ Returns a JSON array of contacts:
|
||||
|
||||
### 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`
|
||||
|
||||
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 +273,9 @@ 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.
|
||||
|
||||
# TODO
|
||||
|
||||
* PendingContactStateChangedEvent
|
||||
* PendingContactRemovedEvent
|
||||
* ContactAddedRemotelyEvent
|
||||
@@ -25,6 +25,7 @@ dependencies {
|
||||
|
||||
testImplementation project(path: ':bramble-api', configuration: 'testOutput')
|
||||
testImplementation project(path: ':bramble-core', configuration: 'testOutput')
|
||||
testImplementation project(path: ':briar-core', configuration: 'testOutput')
|
||||
|
||||
def junitVersion = '5.4.2'
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
|
||||
|
||||
@@ -64,6 +64,16 @@ constructor(
|
||||
path("/v1") {
|
||||
path("/contacts") {
|
||||
get { ctx -> contactController.list(ctx) }
|
||||
path("add") {
|
||||
post { ctx -> contactController.addPendingContact(ctx) }
|
||||
path("link") {
|
||||
get { ctx -> contactController.link(ctx) }
|
||||
}
|
||||
path("pending") {
|
||||
get { ctx -> contactController.listPendingContacts(ctx) }
|
||||
delete { ctx -> contactController.removePendingContact(ctx) }
|
||||
}
|
||||
}
|
||||
path("/:contactId") {
|
||||
delete { ctx -> contactController.delete(ctx) }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import io.javalin.Context
|
||||
interface ContactController {
|
||||
|
||||
fun list(ctx: Context): Context
|
||||
fun link(ctx: Context): Context
|
||||
fun addPendingContact(ctx: Context): Context
|
||||
fun listPendingContacts(ctx: Context): Context
|
||||
fun removePendingContact(ctx: Context): Context
|
||||
fun delete(ctx: Context): Context
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package org.briarproject.briar.headless.contact
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.javalin.Context
|
||||
import io.javalin.NotFoundResponse
|
||||
import org.briarproject.bramble.api.contact.ContactManager
|
||||
import org.briarproject.bramble.api.contact.PendingContactId
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException
|
||||
import org.briarproject.bramble.api.db.NoSuchPendingContactException
|
||||
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
|
||||
@@ -13,7 +20,8 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
internal class ContactControllerImpl
|
||||
@Inject
|
||||
constructor(private val contactManager: ContactManager) : ContactController {
|
||||
constructor(private val contactManager: ContactManager, private val objectMapper: ObjectMapper) :
|
||||
ContactController {
|
||||
|
||||
override fun list(ctx: Context): Context {
|
||||
val contacts = contactManager.contacts.map { contact ->
|
||||
@@ -22,6 +30,44 @@ constructor(private val contactManager: ContactManager) : ContactController {
|
||||
return ctx.json(contacts)
|
||||
}
|
||||
|
||||
override fun link(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")
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.briarproject.briar.headless.contact
|
||||
|
||||
import org.briarproject.bramble.api.contact.PendingContact
|
||||
import org.briarproject.bramble.api.contact.PendingContactState.*
|
||||
import org.briarproject.briar.headless.json.JsonDict
|
||||
|
||||
internal fun PendingContact.output() = JsonDict(
|
||||
"pendingContactId" to id.bytes,
|
||||
"alias" to alias,
|
||||
"state" to when(state) {
|
||||
WAITING_FOR_CONNECTION -> "waiting_for_connection"
|
||||
CONNECTED -> "connected"
|
||||
ADDING_CONTACT -> "adding_contact"
|
||||
FAILED -> "failed"
|
||||
else -> throw AssertionError()
|
||||
},
|
||||
"timestamp" to timestamp
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,18 @@ 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 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"))
|
||||
}
|
||||
|
||||
@@ -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,51 @@ 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 `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", 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 `deleting contact need authentication token`() {
|
||||
val response = deleteWithWrongToken("$url/contacts/1")
|
||||
|
||||
@@ -7,15 +7,21 @@ import io.mockk.every
|
||||
import io.mockk.just
|
||||
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.db.NoSuchContactException
|
||||
import org.briarproject.bramble.api.db.NoSuchPendingContactException
|
||||
import org.briarproject.bramble.identity.output
|
||||
import org.briarproject.bramble.test.TestUtils.getPendingContact
|
||||
import org.briarproject.bramble.test.TestUtils.getRandomBytes
|
||||
import org.briarproject.briar.headless.ControllerTest
|
||||
import org.briarproject.briar.headless.json.JsonDict
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class ContactControllerTest : ControllerTest() {
|
||||
|
||||
private val controller = ContactControllerImpl(contactManager)
|
||||
private val controller = ContactControllerImpl(contactManager, objectMapper)
|
||||
private val pendingContact = getPendingContact()
|
||||
|
||||
@Test
|
||||
fun testEmptyContactList() {
|
||||
@@ -31,6 +37,79 @@ 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.link(ctx)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddPendingContact() {
|
||||
val link = "briar://link123"
|
||||
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 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"
|
||||
@@ -80,4 +159,17 @@ internal class ContactControllerTest : ControllerTest() {
|
||||
assertJsonEquals(json, author.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())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user