Commit 5a73e502 authored by Torsten Grote's avatar Torsten Grote

[headless] expose ContactManager methods for adding contacts remotely

parent dc697173
......@@ -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
\ No newline at end of file
......@@ -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
}
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 {
......
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
)
\ No newline at end of file
......@@ -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"))
}
......
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())
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment