-
-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GH-2179 Switch to using an Server-Sent Event endpoint for Console (Re…
…solve #2120) * rename internal CliEndpoint class to follow file naming * initial implementation * don't hang when stopping server * remove unused sse handler parameter * replace event source library * add todo to error toast * minor cleanup * close all sse connections when shutting down reposilite * ping sse connection every second (closes connection if ping fails) * close sse connection before page is reloaded/refreshed * use an iterator instead of forEach to avoid CME * remove TODO comments about endpoint name * better openapi data * remove unneeded termination check * remove redundant users check * use `ctx.ip()` instead of extension * update connection error toast message * update readystate constant comment * move onbeforeload call into onopen listener * ping every 5 seconds instead of 1 second * disable watcher before closing sse clients * rename consumer function to handleSseLiveLog * stop stack overflow log spam when sse connection is terminated * bump journalist version, fixes CME errors * move watcher into sse handler * minor cleanup * remove final TODO comment
- Loading branch information
Showing
9 changed files
with
178 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
...silite-backend/src/main/kotlin/com/reposilite/console/infrastructure/ConsoleSseHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package com.reposilite.console.infrastructure | ||
|
||
import com.reposilite.ReposiliteJournalist | ||
import com.reposilite.auth.AuthenticationFacade | ||
import com.reposilite.auth.api.Credentials | ||
import com.reposilite.shared.ErrorResponse | ||
import com.reposilite.shared.extractFromHeader | ||
import com.reposilite.shared.unauthorized | ||
import com.reposilite.token.AccessTokenFacade | ||
import com.reposilite.token.AccessTokenPermission | ||
import io.javalin.http.Context | ||
import io.javalin.http.Header | ||
import io.javalin.http.sse.SseClient | ||
import io.javalin.openapi.HttpMethod | ||
import io.javalin.openapi.OpenApi | ||
import io.javalin.openapi.OpenApiParam | ||
import io.javalin.openapi.OpenApiResponse | ||
import panda.std.Result | ||
import panda.std.reactive.Reference | ||
import java.util.* | ||
import java.util.concurrent.ScheduledExecutorService | ||
import java.util.concurrent.ScheduledFuture | ||
import java.util.concurrent.TimeUnit | ||
|
||
private const val SSE_EVENT_NAME = "log" | ||
|
||
data class SseSession( | ||
val identifier: String, | ||
val subscriberId: Int, | ||
val scheduler: ScheduledFuture<*> | ||
) | ||
|
||
internal class ConsoleSseHandler( | ||
private val journalist: ReposiliteJournalist, | ||
private val accessTokenFacade: AccessTokenFacade, | ||
private val authenticationFacade: AuthenticationFacade, | ||
private val forwardedIp: Reference<String>, | ||
private val scheduler: ScheduledExecutorService | ||
) { | ||
|
||
internal val users: WeakHashMap<SseClient, SseSession> = WeakHashMap() | ||
|
||
@OpenApi( | ||
path = "/api/console/log", | ||
methods = [HttpMethod.GET], | ||
headers = [OpenApiParam(name = "Authorization", description = "Name and secret provided as basic auth credentials", required = true)], | ||
description = "Streams the output of logs through an SSE Connection.", | ||
responses = [ | ||
OpenApiResponse( | ||
status = "200", | ||
description = "Continuously sends out the log as messages under the `log` event. Sends a keepalive ping through comments." | ||
) | ||
], | ||
tags = ["Console"] | ||
) | ||
fun handleSseLiveLog(sse: SseClient) { | ||
sse.keepAlive() | ||
sse.onClose { -> | ||
val session = users.remove(sse) ?: return@onClose | ||
session.scheduler.cancel(false) | ||
journalist.logger.info("CLI | ${session.identifier} closed connection") | ||
journalist.unsubscribe(session.subscriberId) | ||
} | ||
|
||
authenticateContext(sse.ctx()) | ||
.peek { identifier -> | ||
journalist.logger.info("CLI | $identifier accessed remote console") | ||
|
||
val subscriberId = journalist.subscribe { | ||
// stop stack overflow log spam | ||
if (!sse.terminated()) { | ||
sse.sendEvent(SSE_EVENT_NAME, it.value) | ||
} | ||
} | ||
|
||
val watcher = scheduler.scheduleWithFixedDelay({ | ||
sse.sendComment("ping") | ||
}, 5, 5, TimeUnit.SECONDS) | ||
|
||
users[sse] = SseSession(identifier, subscriberId, watcher) | ||
|
||
journalist.cachedLogger.messages.forEach { message -> | ||
sse.sendEvent(SSE_EVENT_NAME, message.value) | ||
} | ||
} | ||
.onError { | ||
journalist.logger.info("CLI | ${it.message} (${it.status})") | ||
sse.sendEvent(SSE_EVENT_NAME, it) | ||
sse.close() | ||
} | ||
} | ||
|
||
private fun authenticateContext(connection: Context): Result<String, ErrorResponse> { | ||
return extractFromHeader(connection.header(Header.AUTHORIZATION)) | ||
.map { (name, secret) -> | ||
Credentials( | ||
host = connection.ip(), | ||
name = name, | ||
secret = secret | ||
) | ||
} | ||
.flatMap { authenticationFacade.authenticateByCredentials(it) } | ||
.filter( | ||
{ accessTokenFacade.hasPermission(it.identifier, AccessTokenPermission.MANAGER) }, | ||
{ unauthorized("Unauthorized CLI access request from ${connection.ip()}") } | ||
) | ||
.map { "${it.name}@${connection.ip()}" } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters