briar-headless: Next round of review comments

parent c7eb0cbb
# 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: ...@@ -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: ...@@ -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`. ...@@ -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. ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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" ...@@ -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 { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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(
......
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