diff --git a/api-mocks/__files/logViewer/logs.json b/api-mocks/__files/logViewer/logs.json new file mode 100644 index 000000000..e5c4e7400 --- /dev/null +++ b/api-mocks/__files/logViewer/logs.json @@ -0,0 +1,11 @@ +{ + "logLines": [ + "02:49:12 127.0.0.1 GET / 200", + "02:49:35 127.0.0.1 GET /index.html 200", + "03:01:06 127.0.0.1 GET /images/sponsered.gif 304", + "03:52:36 127.0.0.1 GET /search.php 200", + "04:17:03 127.0.0.1 GET /admin/style.css 200", + "05:04:54 127.0.0.1 GET /favicon.ico 404", + "05:38:07 127.0.0.1 GET /js/ads.js 200" + ] +} \ No newline at end of file diff --git a/api-mocks/mappings/endpoints-mapping.json b/api-mocks/mappings/endpoints-mapping.json index 5a3ade401..d98b991d2 100644 --- a/api-mocks/mappings/endpoints-mapping.json +++ b/api-mocks/mappings/endpoints-mapping.json @@ -1,5 +1,23 @@ { "mappings": [ + { + "request": { + "method": "GET", + "url": "/log-viewer", + "queryParameters": { + "lines": { + "matches": "^[0-9]+$" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "logViewer/logs.json" + } + }, { "request": { "method": "GET", diff --git a/build.gradle.kts b/build.gradle.kts index 4451a9a0c..61c3cb243 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ allprojects { tasks { named("build") { - dependsOn(":cogboard-app:test", ":cogboard-webapp:buildImage") + dependsOn(":cogboard-app:test", ":cogboard-webapp:buildImage", ":ssh:buildImage") } register("cypressInit", Exec::class) { setWorkingDir("./functional/cypress-tests") diff --git a/cogboard-app/build.gradle.kts b/cogboard-app/build.gradle.kts index f1ce38a03..48a4bd342 100644 --- a/cogboard-app/build.gradle.kts +++ b/cogboard-app/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.0") implementation(kotlin("stdlib-jdk8")) implementation("com.jcraft:jsch:0.1.55") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") testImplementation("org.assertj:assertj-core:3.12.2") testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.2") diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt index 10f6cc433..c7f607334 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/CogboardConstants.kt @@ -40,11 +40,13 @@ class CogboardConstants { const val SCHEDULE_DELAY_DEFAULT = 10L // 10 seconds const val SSH_TIMEOUT = 5000 // 5000ms -> 5s const val SSH_HOST = "sshAddress" + const val SSH_PORT = "sshPort" const val SSH_KEY = "sshKey" const val SSH_KEY_PASSPHRASE = "sshKeyPassphrase" const val URL = "url" - const val LOG_LINES = "logLines" - const val LOG_FILE_PATH = "logFilePath" + + const val LOG_REQUEST_TYPE = "logRequestType" + const val LOG_LINES = "logLinesField" const val REQUEST_ID = "requestId" const val PUBLIC_URL = "publicUrl" const val USER = "user" @@ -84,6 +86,13 @@ class CogboardConstants { } } + class ConnectionType { + companion object { + const val SSH = "SSH" + const val HTTP = "HTTP" + } + } + class RequestMethod { companion object { const val GET = "get" diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt index 97609a8c2..bf4712cbe 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/EndpointLoader.kt @@ -31,6 +31,8 @@ class EndpointLoader( this.put(Props.USER, credentials.getString(Props.USER) ?: "") this.put(Props.PASSWORD, credentials.getString(Props.PASSWORD) ?: "") this.put(Props.TOKEN, credentials.getString(Props.TOKEN) ?: "") + this.put(Props.SSH_KEY, credentials.getString(Props.SSH_KEY) ?: "") + this.put(Props.SSH_KEY_PASSPHRASE, credentials.getString(Props.SSH_KEY_PASSPHRASE) ?: "") } } return this diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt index b417d92b6..e9288c042 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/controller/CredentialsController.kt @@ -37,6 +37,8 @@ class CredentialsController : AbstractVerticle() { private fun JsonObject.filterSensitiveData(): JsonObject { this.remove(Props.PASSWORD) + this.remove(Props.SSH_KEY) + this.remove(Props.SSH_KEY_PASSPHRASE) return this } diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt index 19cb04715..6727bf401 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/config/model/Credential.kt @@ -5,5 +5,7 @@ data class Credential( val label: String, val user: String, val password: String?, - val token: String? + val token: String?, + val sshKey: String?, + val sshKeyPassphrase: String? ) diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt index f25b0e32d..1163deb8e 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/SSHClient.kt @@ -13,6 +13,10 @@ import io.vertx.core.eventbus.MessageConsumer import io.vertx.core.json.JsonObject import io.vertx.core.logging.Logger import io.vertx.core.logging.LoggerFactory +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import java.io.InputStream class SSHClient : AbstractVerticle() { @@ -41,13 +45,14 @@ class SSHClient : AbstractVerticle() { } } - private fun tryToConnect(config: JsonObject) { - val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS) - try { - connect(config) - } catch (e: JSchException) { - LOGGER.error(e.message) - vertx.eventBus().send(eventBusAddress, e) + fun tryToConnect(config: JsonObject) { + coroutineScope.launch { + try { + connect(config) + } catch (e: JSchException) { + val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS) + sendError(e, eventBusAddress) + } } } @@ -62,14 +67,17 @@ class SSHClient : AbstractVerticle() { initSSHSession(authData) if (session.isConnected) { createChannel(createCommand()) + } else { + LOGGER.error("Failed to connect to ${authData.host}") } } } private fun initSSHSession(authData: SSHAuthData) { jsch = JSch() - jsch.setKnownHosts("~/.ssh/known_hosts") - val session = SessionStrategyFactory(jsch).create(authData).initSession() + // jsch.setKnownHosts("~/.ssh/known_hosts") for security reasons this should be used + session = SessionStrategyFactory(jsch).create(authData).initSession() + session.setConfig("StrictHostKeyChecking", "no") // not secure session.connect(CogboardConstants.Props.SSH_TIMEOUT) } @@ -83,14 +91,32 @@ class SSHClient : AbstractVerticle() { private fun executeCommandAndSendResult(config: JsonObject) { val eventBusAddress = config.getString(CogboardConstants.Props.EVENT_ADDRESS) - val responseBuffer = Buffer.buffer() - responseBuffer.appendBytes(sshInputStream.readAllBytes()) + val responseBuffer = readResponse() vertx.eventBus().send(eventBusAddress, responseBuffer) channel.disconnect() session.disconnect() } + private fun readResponse(): Buffer { + val responseBuffer = Buffer.buffer() + val tmpBuf = ByteArray(512) + var readBytes = sshInputStream.read(tmpBuf, 0, 512) + while (readBytes != -1) { + responseBuffer.appendBytes(tmpBuf, 0, readBytes) + readBytes = sshInputStream.read(tmpBuf, 0, 512) + } + + return responseBuffer + } + + private fun sendError(e: Exception, eventBusAddress: String) { + LOGGER.error(e.message) + vertx.eventBus().send(eventBusAddress, e.message) + } + companion object { val LOGGER: Logger = LoggerFactory.getLogger(SSHClient::class.java) + + val coroutineScope = CoroutineScope(Job() + Dispatchers.IO) } } diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt index abca29a42..1dfa64105 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/AuthenticationType.kt @@ -2,6 +2,5 @@ package com.cognifide.cogboard.ssh.auth enum class AuthenticationType { BASIC, - TOKEN, SSH_KEY } diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt index b2f6690f6..867adcb9a 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/auth/SSHAuthData.kt @@ -1,25 +1,24 @@ package com.cognifide.cogboard.ssh.auth -import com.cognifide.cogboard.CogboardConstants +import com.cognifide.cogboard.CogboardConstants.Props import com.cognifide.cogboard.ssh.auth.AuthenticationType.BASIC -import com.cognifide.cogboard.ssh.auth.AuthenticationType.TOKEN import com.cognifide.cogboard.ssh.auth.AuthenticationType.SSH_KEY import io.vertx.core.json.Json import io.vertx.core.json.JsonArray import io.vertx.core.json.JsonObject class SSHAuthData(private val config: JsonObject) { - val user = config.getString(CogboardConstants.Props.USER) ?: "" - val password = config.getString(CogboardConstants.Props.PASSWORD) ?: "" - val token = config.getString(CogboardConstants.Props.TOKEN) ?: "" - val key = config.getString(CogboardConstants.Props.SSH_KEY) ?: "" - val host = config.getString(CogboardConstants.Props.SSH_HOST) ?: "" + val user = config.getString(Props.USER) ?: "" + val password = config.getString(Props.PASSWORD) ?: "" + val token = config.getString(Props.TOKEN) ?: "" + val key = config.getString(Props.SSH_KEY) ?: "" + val host = config.getString(Props.SSH_HOST) ?: "" + val port = config.getInteger(Props.SSH_PORT) ?: 22 val authenticationType = fromConfigAuthenticationType() private fun fromConfigAuthenticationType(): AuthenticationType { - val authTypesString = config.getString(CogboardConstants.Props.AUTHENTICATION_TYPES) - - val authTypes = authTypesString?.let { Json.decodeValue(authTypesString) } ?: JsonArray() + val authTypes = config.getString(Props.AUTHENTICATION_TYPES)?.let { + Json.decodeValue(it) } ?: JsonArray() return (authTypes as JsonArray) .map { AuthenticationType.valueOf(it.toString()) } @@ -28,21 +27,19 @@ class SSHAuthData(private val config: JsonObject) { private fun hasAuthTypeCorrectCredentials(authType: AuthenticationType): Boolean = when { - authType == TOKEN && user.isNotBlank() && token.isNotBlank() -> true authType == SSH_KEY && key.isNotBlank() -> true else -> authType == BASIC && user.isNotBlank() && password.isNotBlank() } fun getAuthenticationString(): String = when (authenticationType) { - BASIC -> config.getString(CogboardConstants.Props.PASSWORD) - TOKEN -> config.getString(CogboardConstants.Props.TOKEN) - SSH_KEY -> config.getString(CogboardConstants.Props.SSH_KEY) + BASIC -> config.getString(Props.PASSWORD) + SSH_KEY -> config.getString(Props.SSH_KEY) } fun createCommand(): String { - val logLines = config.getString(CogboardConstants.Props.LOG_LINES) ?: "0" - val logFilePath = config.getString(CogboardConstants.Props.LOG_FILE_PATH) ?: "" + val logLines = config.getInteger(Props.LOG_LINES, 0) + val logFilePath = config.getString(Props.PATH, "") return "cat $logFilePath | tail -$logLines" } diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt index 88b54dd2c..cc7df076e 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/SessionStrategyFactory.kt @@ -1,7 +1,6 @@ package com.cognifide.cogboard.ssh.session import com.cognifide.cogboard.ssh.auth.AuthenticationType.BASIC -import com.cognifide.cogboard.ssh.auth.AuthenticationType.TOKEN import com.cognifide.cogboard.ssh.auth.AuthenticationType.SSH_KEY import com.cognifide.cogboard.ssh.auth.SSHAuthData import com.cognifide.cogboard.ssh.session.strategy.BasicAuthSessionStrategy @@ -12,7 +11,7 @@ import com.jcraft.jsch.JSch class SessionStrategyFactory(private val jsch: JSch) { fun create(authData: SSHAuthData): SessionStrategy = when (authData.authenticationType) { - BASIC, TOKEN -> { + BASIC -> { BasicAuthSessionStrategy(jsch, authData) } SSH_KEY -> { diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt index 96920b28e..119d0293a 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/BasicAuthSessionStrategy.kt @@ -7,7 +7,7 @@ import com.jcraft.jsch.Session class BasicAuthSessionStrategy(jsch: JSch, authData: SSHAuthData) : SessionStrategy(jsch, authData) { override fun initSession(): Session { - val session = jsch.getSession(authData.user, authData.host) + val session = jsch.getSession(authData.user, authData.host, authData.port) session.setPassword(securityString) return session diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt index 65e7c5b72..38133eeca 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/ssh/session/strategy/SSHKeyAuthSessionStrategy.kt @@ -3,15 +3,16 @@ package com.cognifide.cogboard.ssh.session.strategy import com.cognifide.cogboard.ssh.auth.SSHAuthData import com.jcraft.jsch.JSch import com.jcraft.jsch.Session +import io.netty.util.internal.StringUtil.EMPTY_STRING class SSHKeyAuthSessionStrategy(jSch: JSch, authData: SSHAuthData) : SessionStrategy(jSch, authData) { override fun initSession(): Session { - if (authData.password == "") { + if (authData.password == EMPTY_STRING) { jsch.addIdentity(securityString) } else { jsch.addIdentity(securityString, authData.password) } - val session = jsch.getSession(authData.user, authData.host) + val session = jsch.getSession(authData.user, authData.host, authData.port) session.setConfig("PreferredAuthentications", "publickey") return session diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/SSHWidget.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/SSHWidget.kt deleted file mode 100644 index 43939a695..000000000 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/SSHWidget.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.cognifide.cogboard.widget - -import com.cognifide.cogboard.CogboardConstants.Props -import com.cognifide.cogboard.CogboardConstants.Event -import com.cognifide.cogboard.config.service.BoardsConfigService -import io.vertx.core.Vertx -import io.vertx.core.eventbus.MessageConsumer -import io.vertx.core.json.JsonObject -import java.nio.Buffer - -abstract class SSHWidget( - vertx: Vertx, - config: JsonObject, - serv: BoardsConfigService -) : AsyncWidget(vertx, config, serv) { - val sshKey: String = config.endpointProp(Props.SSH_KEY) - val host: String = config.endpointProp(Props.SSH_HOST) - val logPath: String = config.endpointProp(Props.LOG_FILE_PATH) - val logLines: String = config.endpointProp(Props.LOG_LINES) - private lateinit var sshConsumer: MessageConsumer - - fun registerForSSH(eventBusAddress: String) { - sshConsumer = vertx.eventBus() - .consumer(eventBusAddress) - .handler { - handleSSHResponse(it.body()) - } - } - - abstract fun handleSSHResponse(body: Buffer?) - - fun unregisterFromSSH() { - if (::sshConsumer.isInitialized) { - sshConsumer.unregister() - } - } - - fun sendRequestForLogs(config: JsonObject) { - ensureConfigIsPrepared(config) - vertx.eventBus().send(Event.SSH_COMMAND, config) - } - - private fun ensureConfigIsPrepared(config: JsonObject) { - config.getString(Props.USER) ?: config.put(Props.USER, user) - config.getString(Props.PASSWORD) ?: config.put(Props.PASSWORD, password) - config.getString(Props.TOKEN) ?: config.put(Props.TOKEN, token) - config.getString(Props.SSH_KEY) ?: config.put(Props.SSH_KEY, sshKey) - config.getString(Props.SSH_HOST) ?: config.put(Props.SSH_HOST, host) - config.getString(Props.LOG_FILE_PATH) ?: config.put(Props.LOG_FILE_PATH, logPath) - config.getString(Props.LOG_LINES) ?: config.put(Props.LOG_LINES, logLines) - } -} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt index e057a29be..ee5235c1b 100644 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/WidgetIndex.kt @@ -20,7 +20,7 @@ import com.cognifide.cogboard.widget.type.WorldClockWidget import com.cognifide.cogboard.widget.type.randompicker.RandomPickerWidget import com.cognifide.cogboard.widget.type.sonarqube.SonarQubeWidget import com.cognifide.cogboard.widget.type.zabbix.ZabbixWidget -import com.cognifide.cogboard.widget.type.LogViewerWidget +import com.cognifide.cogboard.widget.type.logviewer.LogViewerWidget import io.vertx.core.Vertx import io.vertx.core.json.JsonArray import io.vertx.core.json.JsonObject diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategy.kt new file mode 100644 index 000000000..dcafef062 --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategy.kt @@ -0,0 +1,23 @@ +package com.cognifide.cogboard.widget.connectionStrategy + +import com.cognifide.cogboard.CogboardConstants +import com.cognifide.cogboard.http.auth.AuthenticationType +import io.vertx.core.Vertx +import io.vertx.core.eventbus.MessageConsumer +import io.vertx.core.json.JsonObject + +abstract class ConnectionStrategy(protected val vertx: Vertx, protected val eventBusAddress: String) { + protected fun JsonObject.endpointProp(prop: String): String { + return this.getJsonObject(CogboardConstants.Props.ENDPOINT_LOADED)?.getString(prop) ?: "" + } + + protected open fun authenticationTypes(): Set { + return setOf(AuthenticationType.BASIC) + } + + abstract fun sendRequest(address: String, arguments: JsonObject) + + abstract fun getConsumer(eventBusAddress: String): MessageConsumer<*> + + abstract fun handleResponse(response: Any): String +} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategyFactory.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategyFactory.kt new file mode 100644 index 000000000..b734cb998 --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/ConnectionStrategyFactory.kt @@ -0,0 +1,81 @@ +package com.cognifide.cogboard.widget.connectionStrategy + +import com.cognifide.cogboard.CogboardConstants.ConnectionType.Companion.HTTP +import com.cognifide.cogboard.CogboardConstants.ConnectionType.Companion.SSH +import com.cognifide.cogboard.CogboardConstants.Props +import io.vertx.core.Vertx +import io.vertx.core.json.JsonObject +import java.net.URI + +class ConnectionStrategyFactory( + config: JsonObject, + uri: String +) { + private val connectionType: String + private lateinit var vertx: Vertx + private lateinit var eventBusAddress: String + + init { + connectionType = determineConnectionType(uri, config) + } + + fun addVertxInstance(vertx: Vertx): ConnectionStrategyFactory { + this.vertx = vertx + return this + } + + fun addEventBusAddress(eventBusAddress: String): ConnectionStrategyFactory { + this.eventBusAddress = eventBusAddress + return this + } + + private fun determineConnectionType(uri: String, config: JsonObject): String { + val url = URI.create(uri) + return when (url.scheme) { + "http", "https" -> HTTP + "ssh" -> { + prepareSshConfig(url, config) + SSH + } + else -> { + throw UnknownConnectionTypeException("Unknown strategy type") + } + } + } + + private fun prepareSshConfig(uri: URI, config: JsonObject) { + config.put(Props.SSH_HOST, uri.host) + uri.port.let { + if (it != -1) config.put(Props.SSH_PORT, uri.port) + } + } + + fun checkRequiredParameters() { + var message = "" + when { + !::vertx.isInitialized -> message = "Vertx instance not passed to builder" + !::eventBusAddress.isInitialized -> message = "Eventbus address not passed to builder" + } + + if (message.isNotBlank()) { + throw MissingBuilderParametersException(message) + } + } + + fun build(): ConnectionStrategy { + checkRequiredParameters() + return when (connectionType) { + HTTP -> HttpConnectionStrategy(vertx, eventBusAddress) + SSH -> SSHConnectionStrategy(vertx, eventBusAddress) + else -> throw UnknownConnectionTypeException("Unknown strategy type") + } + } +} + +class UnknownConnectionTypeException( + message: String? +) : RuntimeException(message) + +class MissingBuilderParametersException( + message: String? +) : RuntimeException(message) diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/HttpConnectionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/HttpConnectionStrategy.kt new file mode 100644 index 000000000..c4dc5500a --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/HttpConnectionStrategy.kt @@ -0,0 +1,52 @@ +package com.cognifide.cogboard.widget.connectionStrategy + +import com.cognifide.cogboard.CogboardConstants.Props +import com.cognifide.cogboard.CogboardConstants.Event +import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.GET +import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.PUT +import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.POST +import com.cognifide.cogboard.CogboardConstants.RequestMethod.Companion.DELETE +import io.vertx.core.Vertx +import io.vertx.core.eventbus.MessageConsumer +import io.vertx.core.json.Json +import io.vertx.core.json.JsonObject + +class HttpConnectionStrategy(vertx: Vertx, eventBusAddress: String) : + ConnectionStrategy(vertx, eventBusAddress) { + override fun sendRequest(address: String, arguments: JsonObject) { + when (arguments.getString(Props.LOG_REQUEST_TYPE, "")) { + GET -> vertx.eventBus().send(Event.HTTP_GET, getProps(arguments)) + PUT -> vertx.eventBus().send(Event.HTTP_PUT, putProps(arguments)) + POST -> vertx.eventBus().send(Event.HTTP_POST, postProps(arguments)) + DELETE -> vertx.eventBus().send(Event.HTTP_DELETE, basicProps(arguments)) + } + } + + override fun getConsumer(eventBusAddress: String): MessageConsumer<*> = + vertx.eventBus().consumer(eventBusAddress) + + override fun handleResponse(response: Any): String = + (response as JsonObject).getString(Props.LOG_LINES) + + private fun basicProps(props: JsonObject): JsonObject = + JsonObject() + .put(Props.URL, props.endpointProp(Props.URL)) + .put(Props.EVENT_ADDRESS, eventBusAddress) + .put(Props.USER, props.endpointProp(Props.USER)) + .put(Props.PASSWORD, props.endpointProp(Props.PASSWORD)) + .put(Props.AUTHENTICATION_TYPES, Json.encode(authenticationTypes())) + .put(Props.CONTENT_TYPE, props.endpointProp(Props.CONTENT_TYPE)) + + private fun getProps(props: JsonObject): JsonObject = + basicProps(props) + .put(Props.REQUEST_ID, props.getValue(Props.REQUEST_ID, "")) + .put(Props.TOKEN, props.endpointProp(Props.TOKEN)) + + private fun putProps(props: JsonObject): JsonObject = + basicProps(props) + .put(Props.BODY, props.getJsonObject(Props.BODY)) + + private fun postProps(props: JsonObject): JsonObject = + basicProps(props) + .put(Props.BODY, props.getJsonObject(Props.BODY)) +} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/SSHConnectionStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/SSHConnectionStrategy.kt new file mode 100644 index 000000000..bd1805f9d --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/connectionStrategy/SSHConnectionStrategy.kt @@ -0,0 +1,41 @@ +package com.cognifide.cogboard.widget.connectionStrategy + +import com.cognifide.cogboard.CogboardConstants +import com.cognifide.cogboard.CogboardConstants.Props +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.eventbus.MessageConsumer +import io.vertx.core.json.Json +import io.vertx.core.json.JsonObject +import java.nio.charset.Charset + +open class SSHConnectionStrategy(vertx: Vertx, eventBusAddress: String) : + ConnectionStrategy(vertx, eventBusAddress) { + override fun sendRequest(address: String, arguments: JsonObject) { + val config = prepareConfig(arguments) + vertx.eventBus().send(CogboardConstants.Event.SSH_COMMAND, config) + } + + override fun getConsumer(eventBusAddress: String): MessageConsumer<*> = + vertx.eventBus().consumer(eventBusAddress) + + override fun handleResponse(response: Any): String = + (response as Buffer).toString(Charset.defaultCharset()) + + private fun prepareConfig(config: JsonObject): JsonObject { + val tmpConfig = prepareConfigLines(config = config, + Props.USER, Props.PASSWORD, Props.TOKEN, Props.SSH_KEY, Props.SSH_KEY_PASSPHRASE + ) + + tmpConfig.getString(Props.AUTHENTICATION_TYPES) ?: config.put(Props.AUTHENTICATION_TYPES, Json.encode(authenticationTypes())) + tmpConfig.put(Props.EVENT_ADDRESS, eventBusAddress) + return tmpConfig + } + + private fun prepareConfigLines(config: JsonObject, vararg fields: String): JsonObject { + for (field in fields) { + config.getString(field) ?: config.put(field, config.endpointProp(field)) + } + return config + } +} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/LogViewerWidget.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/LogViewerWidget.kt deleted file mode 100644 index efadc4a68..000000000 --- a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/LogViewerWidget.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.cognifide.cogboard.widget.type - -import com.cognifide.cogboard.config.service.BoardsConfigService -import com.cognifide.cogboard.widget.BaseWidget -import io.vertx.core.Vertx -import io.vertx.core.json.JsonObject - -class LogViewerWidget( - vertx: Vertx, - config: JsonObject, - serv: BoardsConfigService -) : BaseWidget(vertx, config, serv) { - - override fun updateState() { - updateStateByCopingPropsToContent(PROPS) - } - - companion object { - val PROPS = setOf( - "endpoint", - "schedulePeriod", - "path", - "logLinesField", - "logFileSizeField", - "logRecordExpirationField" - ) - } -} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerWidget.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerWidget.kt new file mode 100644 index 000000000..aa0c15adb --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerWidget.kt @@ -0,0 +1,77 @@ +package com.cognifide.cogboard.widget.type.logviewer + +import com.cognifide.cogboard.CogboardConstants.Props +import com.cognifide.cogboard.config.service.BoardsConfigService +import com.cognifide.cogboard.widget.BaseWidget +import com.cognifide.cogboard.widget.Widget +import com.cognifide.cogboard.widget.connectionStrategy.ConnectionStrategy +import com.cognifide.cogboard.widget.connectionStrategy.ConnectionStrategyFactory +import com.cognifide.cogboard.widget.type.logviewer.logparser.LogParserStrategy +import com.cognifide.cogboard.widget.type.logviewer.logparser.LogParserStrategyFactory +import io.vertx.core.Vertx +import io.vertx.core.eventbus.Message +import io.vertx.core.eventbus.MessageConsumer +import io.vertx.core.json.JsonObject + +class LogViewerWidget( + vertx: Vertx, + config: JsonObject, + serv: BoardsConfigService +) : BaseWidget(vertx, config, serv) { + private val address = config.endpointProp(Props.URL) + private var consumer: MessageConsumer<*>? = null + private val connectionStrategy: ConnectionStrategy = determineConnectionStrategy() + private val logParsingStrategy: LogParserStrategy = determineLogParsingStrategy() + + override fun start(): Widget { + consumer = connectionStrategy.getConsumer(eventBusAddress) + consumer!!.handler { + handleResponse(it) + } + return super.start() + } + + override fun stop(): Widget { + consumer?.unregister() + return super.stop() + } + + override fun updateState() { + if (address.isNotBlank()) { + connectionStrategy.sendRequest(address, config) + } else { + sendConfigurationError("Endpoint URL is blank") + } + } + + private fun handleResponse(response: Message<*>) { + val responseBody = response.body() + if (responseBody is JsonObject) { + handleHttpResponse(responseBody) + } else { + send(prepareLogs(connectionStrategy.handleResponse(responseBody))) + } + } + + private fun handleHttpResponse(responseBody: JsonObject) { + if (checkAuthorized(responseBody)) { + send(prepareLogs(connectionStrategy.handleResponse(responseBody))) + } + } + + private fun prepareLogs(logs: String): JsonObject { + var logLines = logs.split("\n") + logLines = logLines.filter { it.isNotEmpty() } + return JsonObject().put("logs", logParsingStrategy.parseLines(logLines)) + } + + private fun determineConnectionStrategy() = + ConnectionStrategyFactory(config, address) + .addVertxInstance(vertx) + .addEventBusAddress(eventBusAddress) + .build() + + private fun determineLogParsingStrategy() = + LogParserStrategyFactory() + .build(LogParserStrategyFactory.MOCK) +} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategy.kt new file mode 100644 index 000000000..a5c41303d --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategy.kt @@ -0,0 +1,17 @@ +package com.cognifide.cogboard.widget.type.logviewer.logparser + +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject + +abstract class LogParserStrategy { + fun parseLines(logLines: Collection): JsonArray { + val resultArray = JsonArray() + for (line in logLines) { + val parsedLine = parseLine(line) + resultArray.add(parsedLine) + } + return resultArray + } + + abstract fun parseLine(logLine: String): JsonObject +} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategyFactory.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategyFactory.kt new file mode 100644 index 000000000..889dcbeb6 --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/LogParserStrategyFactory.kt @@ -0,0 +1,16 @@ +package com.cognifide.cogboard.widget.type.logviewer.logparser + +class LogParserStrategyFactory { + companion object { + const val MOCK = "mock" + } + + fun build(type: String): LogParserStrategy { + return when (type) { + MOCK -> MockLogParserStrategy() + else -> throw UnknownParserTypeException("Unknown strategy type") + } + } +} + +class UnknownParserTypeException(message: String) : RuntimeException(message) diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategy.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategy.kt new file mode 100644 index 000000000..4b0d4dd0d --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategy.kt @@ -0,0 +1,28 @@ +package com.cognifide.cogboard.widget.type.logviewer.logparser + +import io.vertx.core.json.JsonObject +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.TYPE +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.DATE +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.PROVIDER +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.MESSAGE + +class MockLogParserStrategy : LogParserStrategy() { + private val regex = """^(?<$DATE>[0-9-:]+) \*(?<$TYPE>[A-Z]+)\* \[(?<$PROVIDER>[a-zA-Z]+)\][ ]+(?<$MESSAGE>.+)$""".trimMargin().toRegex() + + override fun parseLine(logLine: String): JsonObject { + val groups = regex.matchEntire(logLine.trim())?.groups + + return createLogObject(groups) + } + + private fun createLogObject(groups: MatchGroupCollection?): JsonObject { + val mapOfCapturedValues = mutableMapOf() + mapOfCapturedValues[TYPE] = groups?.get(TYPE)?.value ?: "" + mapOfCapturedValues[DATE] = groups?.get(DATE)?.value ?: "" + mapOfCapturedValues[PROVIDER] = groups?.get(PROVIDER)?.value ?: "" + mapOfCapturedValues[MESSAGE] = groups?.get(MESSAGE)?.value ?: "" + + val parsedLog = ParsedLog(mapOfCapturedValues) + return parsedLog.parsedLogJson + } +} diff --git a/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/ParsedLog.kt b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/ParsedLog.kt new file mode 100644 index 000000000..d9de522f6 --- /dev/null +++ b/cogboard-app/src/main/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/ParsedLog.kt @@ -0,0 +1,63 @@ +package com.cognifide.cogboard.widget.type.logviewer.logparser + +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject + +class ParsedLog(values: Map) { + companion object { + const val DATE = "date" + const val TYPE = "type" + const val PROVIDER = "Provider" + const val MESSAGE = "Message" + const val TEMPLATE = "template" + const val HEADERS = "headers" + const val VARIABLE_DATA = "variableData" + const val DESCRIPTION = "description" + const val ADDITIONAL_DATA = "additionalData" + const val ID = "ID" + const val IP_ADDRESS = "IP address" + const val PORT = "Port" + } + + private val _parsedLogJson = JsonObject() + private val variableData = JsonObject() + + val parsedLogJson: JsonObject + get() { + _parsedLogJson.put(VARIABLE_DATA, variableData) + return _parsedLogJson + } + + init { + values[TYPE]?.let { _parsedLogJson.put(TYPE, it) } + values[DATE]?.let { _parsedLogJson.put(DATE, it) } + values[PROVIDER]?.let { addFieldToVariableData(PROVIDER, it) } + values[MESSAGE]?.let { addFieldToVariableData(MESSAGE, it) } + addAdditionalData() + } + + private fun addFieldToVariableData(template: String, value: String) { + val templateArray = variableData.getJsonArray(TEMPLATE, JsonArray()) + val headersArray = variableData.getJsonArray(HEADERS, JsonArray()) + val descriptionArray = variableData.getJsonArray(DESCRIPTION, JsonArray()) + + if (!templateArray.contains(template)) { + templateArray.add(template) + headersArray.add(value) + descriptionArray.add("No description") + } + variableData.put(TEMPLATE, templateArray) + variableData.put(HEADERS, headersArray) + variableData.put(DESCRIPTION, descriptionArray) + } + + private fun addAdditionalData() { + val additionalData = JsonObject() + additionalData.put(ID, "None") + additionalData.put(TYPE.capitalize(), "None") + additionalData.put(IP_ADDRESS, "None") + additionalData.put(PORT, "None") + + _parsedLogJson.put(ADDITIONAL_DATA, additionalData) + } +} diff --git a/cogboard-app/src/main/resources/initData/credentials.json b/cogboard-app/src/main/resources/initData/credentials.json index db175e834..b3e4b8e77 100644 --- a/cogboard-app/src/main/resources/initData/credentials.json +++ b/cogboard-app/src/main/resources/initData/credentials.json @@ -2,10 +2,11 @@ "credentials": [ { "token": "", + "sshKey": "", "password": "admin", "user": "admin", "label": "Zabbix", "id": "credential1" } ] -} \ No newline at end of file +} diff --git a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt index 2705faf5d..8bcb65644 100644 --- a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt +++ b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/config/EndpointTest.kt @@ -37,10 +37,15 @@ internal class EndpointTest { assert(validEndpoint.containsKey("user")) assert(validEndpoint.containsKey("password")) assert(validEndpoint.containsKey("token")) + assert(validEndpoint.containsKey("sshKey")) + assert(validEndpoint.containsKey("sshKeyPassphrase")) assert(invalidEndpoint.containsKey("user")) assert(invalidEndpoint.containsKey("password")) assert(invalidEndpoint.containsKey("token")) + assert(invalidEndpoint.containsKey("sshKey")) + assert(invalidEndpoint.containsKey("sshKeyPassphrase")) + } @Test @@ -48,6 +53,9 @@ internal class EndpointTest { assertEquals("user1", validEndpoint.getString("user")) assertEquals("password1", validEndpoint.getString("password")) assertEquals("token1", validEndpoint.getString("token")) + assertEquals("key1", validEndpoint.getString("sshKey")) + assertEquals("pass1", validEndpoint.getString("sshKeyPassphrase")) + } @Test @@ -55,6 +63,8 @@ internal class EndpointTest { assertEquals("", invalidEndpoint.getString("user")) assertEquals("", invalidEndpoint.getString("password")) assertEquals("", invalidEndpoint.getString("token")) + assertEquals("", invalidEndpoint.getString("sshKey")) + assertEquals("", invalidEndpoint.getString("sshKeyPassphrase")) } @Test @@ -67,7 +77,9 @@ internal class EndpointTest { "publicUrl" : "Public Url", "user" : "user1", "password" : "password1", - "token" : "token1" + "token" : "token1", + "sshKey": "key1", + "sshKeyPassphrase" : "pass1" } """) @@ -83,7 +95,9 @@ internal class EndpointTest { "url" : "url", "user" : "", "password" : "", - "token" : "" + "token" : "", + "sshKey" : "", + "sshKeyPassphrase" : "" } """) diff --git a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerTest.kt b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerTest.kt new file mode 100644 index 000000000..a06401d31 --- /dev/null +++ b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/LogViewerTest.kt @@ -0,0 +1,63 @@ +package com.cognifide.cogboard.widget.type.logviewer + +import com.cognifide.cogboard.CogboardConstants.Props +import com.cognifide.cogboard.CogboardConstants.RequestMethod +import com.cognifide.cogboard.widget.type.WidgetTestBase +import io.vertx.core.buffer.Buffer +import io.vertx.core.eventbus.MessageConsumer +import io.vertx.core.json.JsonObject +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito.* + +class LogViewerTest: WidgetTestBase() { + + private lateinit var widget: LogViewerWidget + + override fun widgetName(): String { + return "LogViewerWidget" + } + + @BeforeEach + fun initForTest() { + super.init() + } + + @Test + fun `Expect Buffer consumer to be used when type is SSH`() { + val consumerMock = mock(MessageConsumer::class.java) as MessageConsumer + `when`(eventBus.consumer(anyString())).thenReturn(consumerMock) + + val endpoint = mockEndpointData("ssh") + + val config = initWidget() + .put(Props.ENDPOINT_LOADED, endpoint) + .put(Props.LOG_LINES, "5") + widget = LogViewerWidget(vertx, config, initService()) + + widget.start() + + verify(eventBus).consumer(eq(widget.eventBusAddress)) + } + + @Test + fun `Expect JsonObject consumer to be used when type is HTTP`() { + val consumerMock = mock(MessageConsumer::class.java) as MessageConsumer + `when`(eventBus.consumer(anyString())).thenReturn(consumerMock) + + val endpoint = mockEndpointData("http") + + val config = initWidget() + .put(Props.LOG_REQUEST_TYPE, RequestMethod.GET) + .put(Props.ENDPOINT_LOADED, endpoint) + .put(Props.LOG_LINES, "5") + widget = LogViewerWidget(vertx, config, initService()) + + widget.start() + + verify(eventBus).consumer(eq(widget.eventBusAddress)) + } + + private fun mockEndpointData(protocol: String): JsonObject = + JsonObject(mapOf(Pair(Props.URL, "$protocol://192.168.0.1"))) +} diff --git a/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategyTest.kt b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategyTest.kt new file mode 100644 index 000000000..bb56a0fd9 --- /dev/null +++ b/cogboard-app/src/test/kotlin/com/cognifide/cogboard/widget/type/logviewer/logparser/MockLogParserStrategyTest.kt @@ -0,0 +1,30 @@ +package com.cognifide.cogboard.widget.type.logviewer.logparser + +import org.junit.jupiter.api.Test +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.TYPE +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.DATE +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.VARIABLE_DATA +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.HEADERS +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.TEMPLATE +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.PROVIDER +import com.cognifide.cogboard.widget.type.logviewer.logparser.ParsedLog.Companion.MESSAGE + +class MockLogParserStrategyTest { + private val sampleLog = "2021-11-06:22:40:25 *DEBUG* [FelixStartLevel] Integer lobortis. bibendum Nulla mi" + private val parser = MockLogParserStrategy() + + @Test + fun parseSampleLog() { + val output = parser.parseLine(sampleLog) + val variableData = output.getJsonObject(VARIABLE_DATA) + val template = variableData.getJsonArray(TEMPLATE) + val headers = variableData.getJsonArray(HEADERS) + + assert(output.getString(TYPE) == "DEBUG") + assert(output.getString(DATE) == "2021-11-06:22:40:25") + assert(template.getString(0) == PROVIDER) + assert(template.getString(1) == MESSAGE) + assert(headers.getString(0) == "FelixStartLevel") + assert(headers.getString(1) == "Integer lobortis. bibendum Nulla mi") + } +} \ No newline at end of file diff --git a/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json b/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json index 9cdcfe54e..b5b431094 100644 --- a/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json +++ b/cogboard-app/src/test/resources/com/cognifide/cogboard/config/credentials-test.json @@ -5,14 +5,18 @@ "label": "My Credentials 1", "user": "user1", "password": "password1", - "token": "token1" + "token": "token1", + "sshKey": "key1", + "sshKeyPassphrase": "pass1" }, { "id": "credentials2", "label": "My Credentials 2", "user": "user2", "password": "password2", - "token": "token2" + "token": "token2", + "sshKey": "key2", + "sshKeyPassphrase": "pass2" } ] } diff --git a/cogboard-local-compose.yml b/cogboard-local-compose.yml index 327aeb6f9..f9b6fa54a 100644 --- a/cogboard-local-compose.yml +++ b/cogboard-local-compose.yml @@ -16,6 +16,12 @@ services: - cognet command: ["--no-request-journal", "--global-response-templating"] + ssh-server: + image: ssh-server + hostname: ssh-server + networks: + - cognet + backend: image: "cogboard/cogboard-app:${COGBOARD_VERSION}" environment: diff --git a/cogboard-webapp/src/components/CredentialForm/index.js b/cogboard-webapp/src/components/CredentialForm/index.js index 8cc72821a..bd8757fb6 100644 --- a/cogboard-webapp/src/components/CredentialForm/index.js +++ b/cogboard-webapp/src/components/CredentialForm/index.js @@ -22,7 +22,9 @@ const CredentialsForm = ({ 'UsernameField', 'PasswordField', 'PasswordConfirmationField', - 'TokenField' + 'TokenField', + 'SSHKeyField', + 'SSHKeyPassphraseField' ]; const constraints = { @@ -82,7 +84,9 @@ CredentialsForm.defaultProps = { user: '', password: '', confirmationPassword: '', - token: '' + token: '', + sshKey: '', + sshKeyPassphrase: '' }; export default CredentialsForm; diff --git a/cogboard-webapp/src/components/widgets/dialogFields/FileTextInput.js b/cogboard-webapp/src/components/widgets/dialogFields/FileTextInput.js new file mode 100644 index 000000000..e4b774f2f --- /dev/null +++ b/cogboard-webapp/src/components/widgets/dialogFields/FileTextInput.js @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; + +import { Button } from '@material-ui/core'; +import { StyledValidationMessages } from '../../WidgetForm/styled'; +import { + DeleteButton, + StyledHorizontalStack, + StyledLabel, + StyledVerticalStack +} from './styled'; + +const FileTextInput = ({ error, dataCy, onChange }) => { + const [filename, setFilename] = useState(''); + + const getFileContents = async event => { + event.preventDefault(); + const file = event.target.files[0]; + const reader = new FileReader(); + reader.onload = async event => { + const text = event.target.result; + event.target.value = text; + onChange(event); + }; + reader.readAsText(file); + setFilename(file.name); + }; + + const deleteFile = event => { + event.preventDefault(); + setFilename(''); + event.target.value = ''; + onChange(event); + }; + + const fileInfo = + filename === '' ? null : ( + +

{filename}

+ deleteFile(e)}> + Delete + +
+ ); + + return ( + + SSH Key + + + {fileInfo} + + + + ); +}; + +export default FileTextInput; diff --git a/cogboard-webapp/src/components/widgets/dialogFields/index.js b/cogboard-webapp/src/components/widgets/dialogFields/index.js index 2ec23c533..c5891e036 100644 --- a/cogboard-webapp/src/components/widgets/dialogFields/index.js +++ b/cogboard-webapp/src/components/widgets/dialogFields/index.js @@ -37,6 +37,7 @@ import RangeSlider from './RangeSlider'; import LinkListInput from './LinkListInput'; import ToDoListInput from './ToDoListinput'; import WidgetTypeField from './WidgetTypeField'; +import FileTextInput from './FileTextInput'; const dialogFields = { LabelField: { @@ -100,12 +101,33 @@ const dialogFields = { label: 'Token', validator: () => string() }, + SSHKeyField: { + component: FileTextInput, + name: 'sshKey', + label: 'SSH Private Key', + validator: () => + string() + .matches('^-----BEGIN ([A-Z]{1,} )*PRIVATE KEY-----\n', { + message: vm.SSH_KEY_BEGIN, + excludeEmptyString: true + }) + .matches('\n-----END ([A-Z]{1,} )*PRIVATE KEY-----\n$', { + message: vm.SSH_KEY_END, + excludeEmptyString: true + }) + }, + SSHKeyPassphraseField: { + component: PasswordInput, + name: 'sshKeyPassphrase', + label: 'SSH Private Key Passphrase', + validator: () => string() + }, PublicURL: { component: TextInput, name: 'publicUrl', label: 'Public URL', validator: () => - string().matches(/^(http|https|ws|ftp):\/\/.*([:.]).*/, { + string().matches(/^(http|https|ws|ftp|ssh):\/\/.*([:.]).*/, { message: vm.INVALID_PUBLIC_URL(), excludeEmptyString: true }) @@ -251,7 +273,7 @@ const dialogFields = { name: 'url', label: 'URL', validator: () => - string().matches(/^(http|https|ws|ftp):\/\/.*([:.]).*/, { + string().matches(/^(http|https|ws|ftp|ssh):\/\/.*([:.]).*/, { message: vm.INVALID_URL(), excludeEmptyString: true }) diff --git a/cogboard-webapp/src/components/widgets/dialogFields/styled.js b/cogboard-webapp/src/components/widgets/dialogFields/styled.js index 4d56781db..593893e84 100644 --- a/cogboard-webapp/src/components/widgets/dialogFields/styled.js +++ b/cogboard-webapp/src/components/widgets/dialogFields/styled.js @@ -2,7 +2,7 @@ import styled from '@emotion/styled/macro'; import NumberInput from './NumberInput'; import IntegerInput from './IntegerInput'; import { COLORS } from '../../../constants'; -import { Box, Input, Fab, List, FormControl } from '@material-ui/core'; +import { Box, Input, Fab, List, FormControl, Button } from '@material-ui/core'; export const StyledNumberInput = styled(NumberInput)` flex-basis: calc(50% - 18px); @@ -152,3 +152,34 @@ export const StyledMultiLineWrapper = styled.div` flex: 1 0 auto; } `; + +export const StyledHorizontalStack = styled.div` + display: flex; + flex-direction: row; + gap: 12px; +`; + +export const StyledVerticalStack = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +export const StyledLabel = styled.p` + font-size: 1rem; + margin: 0; + color: rgba(255, 255, 255, 0.7); + transform: translate(0, 1.5px) scale(0.75); + transform-origin: top left; + font-weight: 400; + line-height: 1; + letter-spacing: 0.00938em; +`; + +export const DeleteButton = styled(Button)` + background-color: ${COLORS.RED}; + + &:hover { + background-color: ${COLORS.DARK_RED}; + } +`; diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js index 3999565ea..a56b7d487 100644 --- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js +++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/LogEntry.js @@ -34,10 +34,10 @@ export default function LogEntry({ type, date, additionalData, variableData }) { const VariablePart = ({ description }) => { const variableFieldsTemplate = getGridTemplate(variableData.template); - const data = description ? variableData.description : variableData.header; + const data = description ? variableData.description : variableData.headers; return ( - {data.map((text, index) => ( + {data?.map((text, index) => ( {text} ))} @@ -73,7 +73,7 @@ LogEntry.propTypes = { additionalData: objectOf(oneOfType([string, number, bool])), variableData: shape({ template: arrayOf(string).isRequired, - header: arrayOf(oneOfType([string, number, bool])).isRequired, + headers: arrayOf(oneOfType([string, number, bool])).isRequired, description: arrayOf(oneOfType([string, number, bool])).isRequired }) }; @@ -82,7 +82,7 @@ LogEntry.defaultProps = { type: 'info', variableData: { template: [], - header: [], + headers: [], description: [] }, additionalData: {} diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js index 5ff41576a..c85f9192a 100644 --- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js +++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js @@ -11,33 +11,11 @@ import { } from './styled'; import getGridTemplate from './helpers'; -const testLogTemplate = ['Provider', 'Message']; -const testData = { - date: '2021-04-22 14:08:37', - additionalData: { - ID: '123456', - Type: 'sys', - 'IP address': '127.0.0.1', - Port: '27017' - }, - variableData: { - template: testLogTemplate, - header: [ - 'mongodb.log', - 'Expected corresponding JSX closing tag for .' - ], - description: [ - 'provider desc', - 'SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for . (21:6) SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for . (21:6) SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for . (21:6) SyntaxError: /Users/celmer/Documents/js/cogboard/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/index.js: Expected corresponding JSX closing tag for . (21:6)' - ] - } -}; - -export default function LogList() { +export default function LogList({ logs, template }) { const theme = useTheme(); const VariableLogListHeader = () => ( - - {testLogTemplate.map((name, index) => ( + + {template.map((name, index) => ( {name} ))} @@ -54,61 +32,15 @@ export default function LogList() { - {/* static presentation */} - - - - - - - - - - + {logs?.map((log, index) => ( + + ))} ); diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js index 2ff61248c..62669eaf6 100644 --- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js +++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/LogList/styled.js @@ -1,6 +1,7 @@ import styled from '@emotion/styled/macro'; import { COLORS } from '../../../../../constants'; import { Typography, Accordion } from '@material-ui/core'; +import logLevels from '../logLevels'; export const Container = styled.div` max-height: 100%; @@ -36,23 +37,22 @@ export const ColumnTitle = styled(Typography)` `; export const Text = styled(Typography)(props => { - const getColor = type => - ({ - info: COLORS.WHITE, - success: COLORS.GREEN, - warn: COLORS.YELLOW, - error: COLORS.RED - }[type.toLowerCase()]); + let logTypeStyles = ``; + if (props.type) { + const logLevel = logLevels.find( + level => level.value === props.type?.toLowerCase() + ); + logTypeStyles = ` + font-weight: 500; + color: ${logLevel?.color || COLORS.WHITE}; + `; + } return ` line-height: 19px; font-size: 0.8rem; font-weight: 400; - ${props.type && - ` - font-weight: 500; - color: ${getColor(props.type)}; - `} + ${logTypeStyles} `; }); diff --git a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js index 1e6551d1b..851dc8415 100644 --- a/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js +++ b/cogboard-webapp/src/components/widgets/types/LogViewerWidget/Toolbar/FilterPicker/index.js @@ -10,6 +10,7 @@ import { import { ScrollableBox } from './styled'; import ToolbarGroup from '../ToolbarGroup'; import { useState } from 'react'; +import logLevels from '../../logLevels'; const FilterPicker = () => { const handleDelete = name => { @@ -17,7 +18,7 @@ const FilterPicker = () => { }; const [filters, setFilters] = useState([]); - const [logLevel, setLogLevel] = useState(''); + const [logLevel, setLogLevel] = useState('info'); return ( @@ -48,11 +49,11 @@ const FilterPicker = () => { )} > - ALL - DEBUG - INFO - WARN - ERROR + {logLevels.map((level, index) => ( + + {level.value.toUpperCase()} + + ))} @@ -65,11 +66,11 @@ const FilterPicker = () => { value={logLevel} onChange={e => setLogLevel(e.target.value)} > - ALL - DEBUG - INFO - WARN - ERROR + {logLevels.map((level, index) => ( + + {level.value.toUpperCase()} + + ))}