Router.kt 5.22 KB
Newer Older
1 2
package org.briarproject.briar.headless

3 4 5 6
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.ObjectMapper
import io.javalin.BadRequestResponse
import io.javalin.Context
7 8 9
import io.javalin.Javalin
import io.javalin.JavalinEvent.SERVER_START_FAILED
import io.javalin.JavalinEvent.SERVER_STOPPED
10
import io.javalin.NotFoundResponse
11
import io.javalin.apibuilder.ApiBuilder.*
12
import io.javalin.core.util.ContextUtil
13
import io.javalin.core.util.Header.AUTHORIZATION
14
import org.briarproject.bramble.api.contact.ContactId
15
import org.briarproject.briar.headless.blogs.BlogController
16
import org.briarproject.briar.headless.contact.ContactController
17
import org.briarproject.briar.headless.event.WebSocketController
18 19 20
import org.briarproject.briar.headless.forums.ForumController
import org.briarproject.briar.headless.messaging.MessagingController
import java.lang.Runtime.getRuntime
Torsten Grote's avatar
Torsten Grote committed
21
import java.lang.System.exit
22 23
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Logger.getLogger
24 25 26 27 28 29
import javax.annotation.concurrent.Immutable
import javax.inject.Inject
import javax.inject.Singleton

@Immutable
@Singleton
30 31
internal class Router
@Inject
32 33
constructor(
    private val briarService: BriarService,
34
    private val webSocketController: WebSocketController,
35
    private val contactController: ContactController,
36 37 38 39 40
    private val messagingController: MessagingController,
    private val forumController: ForumController,
    private val blogController: BlogController
) {

41 42
    private val logger = getLogger(Router::javaClass.name)
    private val stopped = AtomicBoolean(false)
43

44
    fun start(authToken: String, port: Int, debug: Boolean) {
45
        briarService.start()
46
        getRuntime().addShutdownHook(Thread(this::stop))
47 48

        val app = Javalin.create()
49
            .port(port)
50 51
            .disableStartupBanner()
            .enableCaseSensitiveUrls()
Torsten Grote's avatar
Torsten Grote committed
52 53
            .event(SERVER_START_FAILED) {serverStopped() }
            .event(SERVER_STOPPED) { serverStopped() }
54
        if (debug) app.enableDebugLogging()
55

56
        app.accessManager { handler, ctx, _ ->
57
            if (ctx.header(AUTHORIZATION) == "Bearer $authToken") {
58 59 60 61 62
                handler.handle(ctx)
            } else {
                ctx.status(401).result("Unauthorized")
            }
        }
63
        app.routes {
64 65 66
            path("/v1") {
                path("/contacts") {
                    get { ctx -> contactController.list(ctx) }
67 68 69
                    path("/:contactId") {
                        delete { ctx -> contactController.delete(ctx) }
                    }
70 71 72 73 74 75 76 77 78 79 80 81 82 83
                }
                path("/messages/:contactId") {
                    get { ctx -> messagingController.list(ctx) }
                    post { ctx -> messagingController.write(ctx) }
                }
                path("/forums") {
                    get { ctx -> forumController.list(ctx) }
                    post { ctx -> forumController.create(ctx) }
                }
                path("/blogs") {
                    path("/posts") {
                        get { ctx -> blogController.listPosts(ctx) }
                        post { ctx -> blogController.createPost(ctx) }
                    }
84 85 86
                }
            }
        }
87
        app.ws("/v1/ws") { ws ->
88
            ws.onConnect { session ->
89
                val authHeader = session.header(AUTHORIZATION)
90 91 92 93 94 95 96 97 98 99 100 101 102
                val token = ContextUtil.getBasicAuthCredentials(authHeader)?.username
                if (authToken == token) {
                    logger.info("Adding websocket session with ${session.remoteAddress}")
                    webSocketController.sessions.add(session)
                } else {
                    logger.info("Closing websocket connection with ${session.remoteAddress}")
                    session.close(1008, "Invalid Authentication Token")
                }
            }
            ws.onClose { session, _, _ ->
                logger.info("Removing websocket connection with ${session.remoteAddress}")
                webSocketController.sessions.remove(session)
            }
103
        }
104
        app.start()
105 106
    }

Torsten Grote's avatar
Torsten Grote committed
107 108 109 110 111
    private fun serverStopped() {
        stop()
        exit(1)
    }

112
    private fun stop() {
113 114 115
        if (!stopped.getAndSet(true)) {
            briarService.stop()
        }
116 117 118
    }

}
119

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
/**
 * Returns a [ContactId] from the "contactId" path parameter.
 *
 * @throws NotFoundResponse when contactId is not a number.
 */
fun Context.getContactIdFromPathParam(): ContactId {
    val contactString = pathParam("contactId")
    val contactInt = try {
        Integer.parseInt(contactString)
    } catch (e: NumberFormatException) {
        throw NotFoundResponse()
    }
    return ContactId(contactInt)
}

135 136 137
/**
 * Returns a String from the JSON field or throws [BadRequestResponse] if null or empty.
 */
138
fun Context.getFromJson(objectMapper: ObjectMapper, field: String) : String {
139
    try {
140
        val jsonNode = objectMapper.readTree(body())
141 142 143 144 145 146 147 148
        if (!jsonNode.hasNonNull(field)) throw BadRequestResponse("'$field' missing in JSON")
        val result = jsonNode.get(field).asText()
        if (result == null || result.isEmpty()) throw BadRequestResponse("'$field' empty in JSON")
        return result
    } catch (e: JsonParseException) {
        throw BadRequestResponse("Invalid JSON")
    }
}