mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-18 05:39:53 +01:00
briar-headless: Next round of review comments
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
# Briar REST API
|
# Briar REST API
|
||||||
|
|
||||||
This is a headless Briar client that exposes a REST API
|
This is a headless Briar peer that exposes a REST API
|
||||||
with an integrated HTTP server instead of a traditional user interface.
|
with an integrated HTTP server instead of a traditional user interface.
|
||||||
You can use this API to script the client behavior
|
You can use this API to script the peer behavior
|
||||||
or to develop your own user interface for it.
|
or to develop your own user interface for it.
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
The REST API client comes as a `jar` file
|
The REST API peer comes as a `jar` file
|
||||||
and needs a Java Runtime Environment (JRE) that supports at least Java 8.
|
and needs a Java Runtime Environment (JRE) that supports at least Java 8.
|
||||||
It currently works only on GNU/Linux operating systems.
|
It currently works only on GNU/Linux operating systems.
|
||||||
|
|
||||||
You can start the client (and its API server) like this:
|
You can start the peer (and its API server) like this:
|
||||||
|
|
||||||
$ java -jar briar-headless/build/libs/briar-headless.jar
|
$ java -jar briar-headless/build/libs/briar-headless.jar
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ Returns a JSON array of contacts:
|
|||||||
*Not yet implemented*
|
*Not yet implemented*
|
||||||
|
|
||||||
The only workaround is to add a contact to the Briar app running on a rooted Android phone
|
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 client.
|
and then move its database (and key files) to the headless peer.
|
||||||
|
|
||||||
### Listing all private messages
|
### Listing all private messages
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ It returns a JSON array of private messages:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If `local` is `true`, the message was sent by the Briar client instead of its remote contact.
|
If `local` is `true`, the message was sent by the Briar peer instead of its remote contact.
|
||||||
|
|
||||||
Attention: There can messages of other `type`s where the message `body` is `null`.
|
Attention: There can messages of other `type`s where the message `body` is `null`.
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ The text of the blog post should be included in the form parameter `text`.
|
|||||||
|
|
||||||
## Websocket API
|
## Websocket API
|
||||||
|
|
||||||
The Briar client uses a websocket to notify a connected API client about new events.
|
The Briar peer uses a websocket to notify a connected API client about new events.
|
||||||
|
|
||||||
`WS /v1/ws`
|
`WS /v1/ws`
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ Your websocket client will most likely add these headers automatically.
|
|||||||
|
|
||||||
### Receiving new private messages
|
### Receiving new private messages
|
||||||
|
|
||||||
When the Briar client receives a new private message,
|
When the Briar peer receives a new private message,
|
||||||
it will send a JSON object to connected websocket clients:
|
it will send a JSON object to connected websocket clients:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import javax.inject.Singleton
|
|||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class BriarService
|
internal class BriarService
|
||||||
@Inject constructor(
|
@Inject
|
||||||
|
constructor(
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val lifecycleManager: LifecycleManager,
|
private val lifecycleManager: LifecycleManager,
|
||||||
private val passwordStrengthEstimator: PasswordStrengthEstimator
|
private val passwordStrengthEstimator: PasswordStrengthEstimator
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import io.javalin.JavalinEvent.SERVER_START_FAILED
|
|||||||
import io.javalin.JavalinEvent.SERVER_STOPPED
|
import io.javalin.JavalinEvent.SERVER_STOPPED
|
||||||
import io.javalin.apibuilder.ApiBuilder.*
|
import io.javalin.apibuilder.ApiBuilder.*
|
||||||
import io.javalin.core.util.ContextUtil
|
import io.javalin.core.util.ContextUtil
|
||||||
import io.javalin.core.util.Header
|
import io.javalin.core.util.Header.AUTHORIZATION
|
||||||
import org.briarproject.briar.headless.blogs.BlogController
|
import org.briarproject.briar.headless.blogs.BlogController
|
||||||
import org.briarproject.briar.headless.contact.ContactController
|
import org.briarproject.briar.headless.contact.ContactController
|
||||||
import org.briarproject.briar.headless.event.WebSocketController
|
import org.briarproject.briar.headless.event.WebSocketController
|
||||||
@@ -21,7 +21,8 @@ import kotlin.system.exitProcess
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class Router @Inject
|
internal class Router
|
||||||
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
private val briarService: BriarService,
|
private val briarService: BriarService,
|
||||||
private val webSocketController: WebSocketController,
|
private val webSocketController: WebSocketController,
|
||||||
@@ -36,7 +37,7 @@ constructor(
|
|||||||
|
|
||||||
fun start(authToken: String, port: Int, debug: Boolean) {
|
fun start(authToken: String, port: Int, debug: Boolean) {
|
||||||
briarService.start()
|
briarService.start()
|
||||||
getRuntime().addShutdownHook(Thread(Runnable { stop() }))
|
getRuntime().addShutdownHook(Thread(this::stop))
|
||||||
|
|
||||||
val app = Javalin.create()
|
val app = Javalin.create()
|
||||||
.port(port)
|
.port(port)
|
||||||
@@ -48,7 +49,7 @@ constructor(
|
|||||||
app.start()
|
app.start()
|
||||||
|
|
||||||
app.accessManager { handler, ctx, _ ->
|
app.accessManager { handler, ctx, _ ->
|
||||||
if (ctx.header("Authorization") == "Bearer $authToken") {
|
if (ctx.header(AUTHORIZATION) == "Bearer $authToken") {
|
||||||
handler.handle(ctx)
|
handler.handle(ctx)
|
||||||
} else {
|
} else {
|
||||||
ctx.status(401).result("Unauthorized")
|
ctx.status(401).result("Unauthorized")
|
||||||
@@ -77,7 +78,7 @@ constructor(
|
|||||||
}
|
}
|
||||||
app.ws("/v1/ws") { ws ->
|
app.ws("/v1/ws") { ws ->
|
||||||
ws.onConnect { session ->
|
ws.onConnect { session ->
|
||||||
val authHeader = session.header(Header.AUTHORIZATION)
|
val authHeader = session.header(AUTHORIZATION)
|
||||||
val token = ContextUtil.getBasicAuthCredentials(authHeader)?.username
|
val token = ContextUtil.getBasicAuthCredentials(authHeader)?.username
|
||||||
if (authToken == token) {
|
if (authToken == token) {
|
||||||
logger.info("Adding websocket session with ${session.remoteAddress}")
|
logger.info("Adding websocket session with ${session.remoteAddress}")
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import javax.inject.Singleton
|
|||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class BlogControllerImpl
|
internal class BlogControllerImpl
|
||||||
@Inject constructor(
|
@Inject
|
||||||
|
constructor(
|
||||||
private val blogManager: BlogManager,
|
private val blogManager: BlogManager,
|
||||||
private val blogPostFactory: BlogPostFactory,
|
private val blogPostFactory: BlogPostFactory,
|
||||||
private val identityManager: IdentityManager,
|
private val identityManager: IdentityManager,
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import javax.inject.Singleton
|
|||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class ContactControllerImpl
|
internal class ContactControllerImpl
|
||||||
@Inject constructor(private val contactManager: ContactManager) : ContactController {
|
@Inject
|
||||||
|
constructor(private val contactManager: ContactManager) : ContactController {
|
||||||
|
|
||||||
override fun list(ctx: Context): Context {
|
override fun list(ctx: Context): Context {
|
||||||
val contacts = contactManager.activeContacts.map { contact ->
|
val contacts = contactManager.activeContacts.map { contact ->
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.headless.event
|
|||||||
|
|
||||||
import io.javalin.websocket.WsSession
|
import io.javalin.websocket.WsSession
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor
|
import org.briarproject.bramble.api.lifecycle.IoExecutor
|
||||||
|
import org.briarproject.briar.headless.json.JsonDict
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
|
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
@@ -12,6 +13,6 @@ interface WebSocketController {
|
|||||||
/**
|
/**
|
||||||
* Sends an event to all open sessions using the [IoExecutor].
|
* Sends an event to all open sessions using the [IoExecutor].
|
||||||
*/
|
*/
|
||||||
fun sendEvent(name: String, obj: Any)
|
fun sendEvent(name: String, obj: JsonDict)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import io.javalin.json.JavalinJson.toJson
|
|||||||
import io.javalin.websocket.WsSession
|
import io.javalin.websocket.WsSession
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor
|
import org.briarproject.bramble.api.lifecycle.IoExecutor
|
||||||
import org.briarproject.bramble.util.LogUtils.logException
|
import org.briarproject.bramble.util.LogUtils.logException
|
||||||
|
import org.briarproject.briar.headless.json.JsonDict
|
||||||
import org.eclipse.jetty.websocket.api.WebSocketException
|
import org.eclipse.jetty.websocket.api.WebSocketException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level.WARNING
|
||||||
import java.util.logging.Logger.getLogger
|
import java.util.logging.Logger.getLogger
|
||||||
import javax.annotation.concurrent.Immutable
|
import javax.annotation.concurrent.Immutable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -17,21 +18,24 @@ import javax.inject.Singleton
|
|||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class WebSocketControllerImpl
|
internal class WebSocketControllerImpl
|
||||||
@Inject constructor(@IoExecutor private val ioExecutor: Executor) : WebSocketController {
|
@Inject
|
||||||
|
constructor(@IoExecutor private val ioExecutor: Executor) : WebSocketController {
|
||||||
|
|
||||||
private val logger = getLogger(WebSocketControllerImpl::javaClass.name)
|
private val logger = getLogger(WebSocketControllerImpl::javaClass.name)
|
||||||
|
|
||||||
override val sessions: MutableSet<WsSession> = ConcurrentHashMap.newKeySet<WsSession>()
|
override val sessions: MutableSet<WsSession> = ConcurrentHashMap.newKeySet<WsSession>()
|
||||||
|
|
||||||
override fun sendEvent(name: String, obj: Any) = ioExecutor.execute {
|
override fun sendEvent(name: String, obj: JsonDict) {
|
||||||
|
val event = toJson(OutputEvent(name, obj))
|
||||||
sessions.forEach { session ->
|
sessions.forEach { session ->
|
||||||
val event = OutputEvent(name, obj)
|
ioExecutor.execute {
|
||||||
try {
|
try {
|
||||||
session.send(toJson(event))
|
session.send(event)
|
||||||
} catch (e: WebSocketException) {
|
} catch (e: WebSocketException) {
|
||||||
logException(logger, Level.WARNING, e)
|
logException(logger, WARNING, e)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
logException(logger, Level.WARNING, e)
|
logException(logger, WARNING, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import javax.inject.Singleton
|
|||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class ForumControllerImpl
|
internal class ForumControllerImpl
|
||||||
@Inject constructor(private val forumManager: ForumManager) : ForumController {
|
@Inject
|
||||||
|
constructor(private val forumManager: ForumManager) : ForumController {
|
||||||
|
|
||||||
override fun list(ctx: Context): Context {
|
override fun list(ctx: Context): Context {
|
||||||
return ctx.json(forumManager.forums.output())
|
return ctx.json(forumManager.forums.output())
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ internal const val EVENT_PRIVATE_MESSAGE = "PrivateMessageReceivedEvent"
|
|||||||
@Immutable
|
@Immutable
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class MessagingControllerImpl
|
internal class MessagingControllerImpl
|
||||||
@Inject constructor(
|
@Inject
|
||||||
|
constructor(
|
||||||
private val messagingManager: MessagingManager,
|
private val messagingManager: MessagingManager,
|
||||||
private val conversationManager: ConversationManager,
|
private val conversationManager: ConversationManager,
|
||||||
private val privateMessageFactory: PrivateMessageFactory,
|
private val privateMessageFactory: PrivateMessageFactory,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ internal fun BlogInvitationResponse.output(contactId: ContactId): JsonDict {
|
|||||||
|
|
||||||
internal fun ForumInvitationResponse.output(contactId: ContactId): JsonDict {
|
internal fun ForumInvitationResponse.output(contactId: ContactId): JsonDict {
|
||||||
val dict = (this as InvitationResponse).output(contactId)
|
val dict = (this as InvitationResponse).output(contactId)
|
||||||
dict["type"] = "BlogInvitationResponse"
|
dict["type"] = "ForumInvitationResponse"
|
||||||
return dict
|
return dict
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ internal class MessagingControllerImplTest : ControllerTest() {
|
|||||||
private val header =
|
private val header =
|
||||||
PrivateMessageHeader(message.id, group.id, timestamp, true, true, true, true)
|
PrivateMessageHeader(message.id, group.id, timestamp, true, true, true, true)
|
||||||
private val sessionId = SessionId(getRandomId())
|
private val sessionId = SessionId(getRandomId())
|
||||||
|
private val privateMessage = PrivateMessage(message)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun list() {
|
fun list() {
|
||||||
@@ -91,7 +92,6 @@ internal class MessagingControllerImplTest : ControllerTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun write() {
|
fun write() {
|
||||||
val privateMessage = PrivateMessage(message)
|
|
||||||
val slot = CapturingSlot<JsonDict>()
|
val slot = CapturingSlot<JsonDict>()
|
||||||
|
|
||||||
expectGetContact()
|
expectGetContact()
|
||||||
@@ -110,12 +110,7 @@ internal class MessagingControllerImplTest : ControllerTest() {
|
|||||||
|
|
||||||
controller.write(ctx)
|
controller.write(ctx)
|
||||||
|
|
||||||
val output = slot.captured
|
assertEquals(privateMessage.output(contact.id, body), slot.captured)
|
||||||
assertEquals(privateMessage.output(contact.id, body), output)
|
|
||||||
assertEquals(contact.id.int, output["contactId"])
|
|
||||||
assertEquals(body, output["body"])
|
|
||||||
assertEquals(message.id.bytes, output["id"])
|
|
||||||
assertEquals("PrivateMessage", output["type"])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -181,6 +176,25 @@ internal class MessagingControllerImplTest : ControllerTest() {
|
|||||||
assertJsonEquals(json, header.output(contact.id, body))
|
assertJsonEquals(json, header.output(contact.id, body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOutputPrivateMessage() {
|
||||||
|
val json = """
|
||||||
|
{
|
||||||
|
"body": "$body",
|
||||||
|
"type": "PrivateMessage",
|
||||||
|
"timestamp": ${message.timestamp},
|
||||||
|
"groupId": ${toJson(message.groupId.bytes)},
|
||||||
|
"contactId": ${contact.id.int},
|
||||||
|
"local": true,
|
||||||
|
"seen": true,
|
||||||
|
"read": true,
|
||||||
|
"sent": true,
|
||||||
|
"id": ${toJson(message.id.bytes)}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
assertJsonEquals(json, privateMessage.output(contact.id, body))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIntroductionRequestWithEmptyBody() {
|
fun testIntroductionRequestWithEmptyBody() {
|
||||||
val request = IntroductionRequest(
|
val request = IntroductionRequest(
|
||||||
|
|||||||
Reference in New Issue
Block a user