diff --git a/applications/tools/metrics-webserver/src/main/kotlin/net/corda/metrics/reader/MetricsReaderMain.kt b/applications/tools/metrics-webserver/src/main/kotlin/net/corda/metrics/reader/MetricsReaderMain.kt index 98355093a63..a38dd9f6f23 100644 --- a/applications/tools/metrics-webserver/src/main/kotlin/net/corda/metrics/reader/MetricsReaderMain.kt +++ b/applications/tools/metrics-webserver/src/main/kotlin/net/corda/metrics/reader/MetricsReaderMain.kt @@ -1,8 +1,8 @@ package net.corda.metrics.reader import io.javalin.Javalin -import io.javalin.core.util.Header import io.javalin.http.HandlerType +import io.javalin.http.Header import org.slf4j.LoggerFactory import java.nio.file.Files import java.nio.file.Path @@ -27,7 +27,7 @@ class MetricsReaderMain { loadMeasurements(Paths.get(metricsFile)) server = Javalin.create() - server?.addHandler(HandlerType.GET, "/metrics") { context -> + server?.addHttpHandler(HandlerType.GET, "/metrics") { context -> context.result(nextReading()) context.header(Header.CACHE_CONTROL, "no-cache") } diff --git a/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Health.kt b/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Health.kt index a3c653de6ef..f20de1a73b3 100644 --- a/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Health.kt +++ b/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Health.kt @@ -1,7 +1,7 @@ package net.corda.applications.workers.workercommon import com.fasterxml.jackson.databind.ObjectMapper -import io.javalin.core.util.Header +import io.javalin.http.Header import net.corda.lifecycle.LifecycleStatus import net.corda.lifecycle.registry.LifecycleRegistry import net.corda.rest.ResponseCode diff --git a/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Metrics.kt b/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Metrics.kt index 5bab13a9a8a..e28513f6d3a 100644 --- a/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Metrics.kt +++ b/applications/workers/worker-common/src/main/kotlin/net/corda/applications/workers/workercommon/Metrics.kt @@ -1,6 +1,6 @@ package net.corda.applications.workers.workercommon -import io.javalin.core.util.Header +import io.javalin.http.Header import io.micrometer.core.instrument.binder.jvm.ClassLoaderMetrics import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1b9c401298..c4c53297107 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,11 +10,11 @@ commonsioVersion = "2.16.1" guavaVersion = "33.2.1-jre" hikariCpVersion = "5.1.0" jacksonVersion = "2.17.2" -javalinVersion = "4.6.8" +javalinVersion = "6.2.0" jcipAnnotationsVersion = "1.0_2" # This version of Jetty must be the same major version as used by Javalin, please see above. # Once Javalin version is upgraded to the latest, this override may be removed. -jettyVersion = "9.4.55.v20240627" +jettyVersion = "11.0.21" micrometerVersion = "1.12.4" nimbusVersion = "11.13" kafkaClientVersion = "3.7.0_1" @@ -66,7 +66,7 @@ assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "as bouncycastle-pkix = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bouncycastleVersion" } bouncycastle-prov = { group = "org.bouncycastle", name = "bcprov-jdk18on", version.ref = "bouncycastleVersion" } brave-context-slf4j = { group = "io.zipkin.brave", name = "brave-context-slf4j", version.ref = "braveVersion" } -brave-instrumentation-servlet = { group = "io.zipkin.brave", name = "brave-instrumentation-servlet", version.ref = "braveVersion" } +brave-instrumentation-servlet = { group = "io.zipkin.brave", name = "brave-instrumentation-servlet-jakarta", version.ref = "braveVersion" } caffeine = { group = "com.github.ben-manes.caffeine", name = "caffeine", version.ref = "caffeineVersion" } commonsio = { group = "commons-io", name = "commons-io", version.ref = "commonsioVersion" } guava = { group = "com.google.guava", name = "guava", version.ref = "guavaVersion" } @@ -83,7 +83,7 @@ jetty-server = { group = "org.eclipse.jetty", name = "jetty-server", version.ref jetty-xml = { group = "org.eclipse.jetty", name = "jetty-xml", version.ref = "jettyVersion" } jetty-websocket-servlet = { group = "org.eclipse.jetty.websocket", name = "websocket-servlet", version.ref = "jettyVersion" } jetty-websocket-server = { group = "org.eclipse.jetty.websocket", name = "websocket-server", version.ref = "jettyVersion" } -jetty-websocket-client = { group = "org.eclipse.jetty.websocket", name = "websocket-client", version.ref = "jettyVersion" } +jetty-websocket-client = { group = "org.eclipse.jetty.websocket", name = "websocket-jetty-client", version.ref = "jettyVersion" } jetty-http2-server = { group = "org.eclipse.jetty.http2", name = "http2-server", version.ref = "jettyVersion" } junit = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junitVersion" } junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junitVersion" } diff --git a/libs/crypto/certificate-generation/src/main/kotlin/net/corda/crypto/test/certificates/generation/RevocableCertificateAuthorityImpl.kt b/libs/crypto/certificate-generation/src/main/kotlin/net/corda/crypto/test/certificates/generation/RevocableCertificateAuthorityImpl.kt index 2af41d89532..34be8932224 100644 --- a/libs/crypto/certificate-generation/src/main/kotlin/net/corda/crypto/test/certificates/generation/RevocableCertificateAuthorityImpl.kt +++ b/libs/crypto/certificate-generation/src/main/kotlin/net/corda/crypto/test/certificates/generation/RevocableCertificateAuthorityImpl.kt @@ -30,7 +30,7 @@ internal class RevocableCertificateAuthorityImpl( internal const val PATH = "/ocsp" } private val app = Javalin.create().start(port) - .addHandler(HandlerType.GET, "$PATH/*", OcspHandler()) + .addHttpHandler(HandlerType.GET, "$PATH/*", OcspHandler()) private val revokedCertificates = ConcurrentHashMap.newKeySet() private val clock = Clock.systemUTC() @@ -51,7 +51,7 @@ internal class RevocableCertificateAuthorityImpl( } override fun close() { - app.close() + app.stop() } private inner class OcspHandler : Handler { diff --git a/libs/rest/generated-rest-client/src/test/kotlin/net/corda/restclient/generated/TestClientAuthHeader.kt b/libs/rest/generated-rest-client/src/test/kotlin/net/corda/restclient/generated/TestClientAuthHeader.kt index 106eb0b5571..aa0697c41c2 100644 --- a/libs/rest/generated-rest-client/src/test/kotlin/net/corda/restclient/generated/TestClientAuthHeader.kt +++ b/libs/rest/generated-rest-client/src/test/kotlin/net/corda/restclient/generated/TestClientAuthHeader.kt @@ -2,6 +2,8 @@ package net.corda.restclient.generated import io.javalin.Javalin import io.javalin.http.ContentType +import io.javalin.http.Header +import io.javalin.http.servlet.getBasicAuthCredentials import net.corda.restclient.CordaRestClient import net.corda.restclient.generated.infrastructure.ApiClient import org.assertj.core.api.AssertionsForClassTypes.assertThat @@ -22,12 +24,12 @@ class TestClientAuthHeader { app = Javalin.create().start(0) app.get("api/v5_3/mgm/1234/info") { ctx -> - assertThat(ctx.basicAuthCredentialsExist()).isTrue() + val basicAuthCredentials = getBasicAuthCredentials(ctx.header(Header.AUTHORIZATION)) + assertThat(basicAuthCredentials).isNotNull() // Respond with the credentials from the basic auth header - val (user, password) = ctx.basicAuthCredentials() ctx.contentType(ContentType.APPLICATION_JSON) - .result("$user:$password") + .result("${basicAuthCredentials?.username}:${basicAuthCredentials?.password}") } } diff --git a/libs/rest/rest-server-impl/build.gradle b/libs/rest/rest-server-impl/build.gradle index cf4a15c97dd..c7283ad4466 100644 --- a/libs/rest/rest-server-impl/build.gradle +++ b/libs/rest/rest-server-impl/build.gradle @@ -29,16 +29,11 @@ dependencies { implementation project(":libs:rest:rest-server") implementation "net.corda:corda-crypto" implementation libs.javalin - constraints { - implementation(libs.bundles.jetty) { - because 'Javalin uses an older version of Jetty which is exposed to CVE-2023-26048 and CVE-2023-26049. ' + - 'This might be resolved in the future versions of Javalin.' - } - } implementation libs.nimbus.sdk implementation libs.jcip.annotations implementation libs.swagger.core implementation libs.jetty.http2.server + implementation libs.jetty.websocket.client implementation libs.jackson.module.kotlin implementation project(":libs:rest:rest-common") diff --git a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/AbstractWebsocketTest.kt b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/AbstractWebsocketTest.kt index 25f890ad039..7e411ffcd06 100644 --- a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/AbstractWebsocketTest.kt +++ b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/AbstractWebsocketTest.kt @@ -1,21 +1,23 @@ package net.corda.rest.server.impl -import io.javalin.core.util.Header +import io.javalin.http.Header import net.corda.rest.server.config.models.RestServerSettings import net.corda.rest.test.utils.WebRequest import net.corda.rest.tools.HttpVerb +import net.corda.utilities.debug import org.apache.http.HttpStatus import org.assertj.core.api.Assertions.assertThat +import org.eclipse.jetty.client.HttpRequest +import org.eclipse.jetty.client.HttpResponse +import org.eclipse.jetty.http.HttpField import org.eclipse.jetty.io.EofException import org.eclipse.jetty.websocket.api.CloseStatus import org.eclipse.jetty.websocket.api.Session import org.eclipse.jetty.websocket.api.StatusCode -import org.eclipse.jetty.websocket.api.UpgradeRequest -import org.eclipse.jetty.websocket.api.UpgradeResponse +import org.eclipse.jetty.websocket.api.WebSocketAdapter import org.eclipse.jetty.websocket.client.ClientUpgradeRequest -import org.eclipse.jetty.websocket.client.NoOpEndpoint +import org.eclipse.jetty.websocket.client.JettyUpgradeListener import org.eclipse.jetty.websocket.client.WebSocketClient -import org.eclipse.jetty.websocket.client.io.UpgradeListener import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.slf4j.Logger @@ -50,7 +52,8 @@ abstract class AbstractWebsocketTest : RestServerTestBase() { fun `valid path returns 200 OK`() { val getPathResponse = client.call(HttpVerb.GET, WebRequest("health/sanity"), userName, password) assertEquals(HttpStatus.SC_OK, getPathResponse.responseStatus) - assertEquals("localhost", getPathResponse.headers[Header.ACCESS_CONTROL_ALLOW_ORIGIN]) + + assertEquals("http://localhost", getPathResponse.headers[Header.ACCESS_CONTROL_ALLOW_ORIGIN]) assertEquals("true", getPathResponse.headers[Header.ACCESS_CONTROL_ALLOW_CREDENTIALS]) assertEquals("no-cache", getPathResponse.headers[Header.CACHE_CONTROL]) } @@ -65,7 +68,7 @@ abstract class AbstractWebsocketTest : RestServerTestBase() { val maximumDesiredCount = 100 val list = mutableListOf() - val wsHandler = object : NoOpEndpoint() { + val wsHandler = object : WebSocketAdapter() { override fun onWebSocketConnect(session: Session) { log.info("onWebSocketConnect : $session") @@ -78,6 +81,7 @@ abstract class AbstractWebsocketTest : RestServerTestBase() { } override fun onWebSocketText(message: String) { + log.debug { "Receiving: $message" } list.add(message) if (list.size >= maximumDesiredCount) { log.warn("Too many received!") @@ -86,14 +90,14 @@ abstract class AbstractWebsocketTest : RestServerTestBase() { } } - val upgradeListener = object : UpgradeListener { - override fun onHandshakeRequest(request: UpgradeRequest) { + val upgradeListener = object : JettyUpgradeListener { + override fun onHandshakeRequest(request: HttpRequest) { val headerValue = toBasicAuthValue(userName, password) log.info("Header value: $headerValue") - request.setHeader(Header.AUTHORIZATION, headerValue) + request.addHeader(HttpField(Header.AUTHORIZATION, headerValue)) } - override fun onHandshakeResponse(response: UpgradeResponse) { + override fun onHandshakeResponse(request: HttpRequest, response: HttpResponse) { } } @@ -130,12 +134,12 @@ abstract class AbstractWebsocketTest : RestServerTestBase() { @Test fun `check WebSocket wrong credentials connectivity`() { - val upgradeListener = object : UpgradeListener { - override fun onHandshakeRequest(request: UpgradeRequest) { - request.setHeader(Header.AUTHORIZATION, toBasicAuthValue("alienUser", "wrongPassword")) + val upgradeListener = object : JettyUpgradeListener { + override fun onHandshakeRequest(request: HttpRequest) { + request.addHeader(HttpField(Header.AUTHORIZATION, toBasicAuthValue("alienUser", "wrongPassword"))) } - override fun onHandshakeResponse(response: UpgradeResponse) { + override fun onHandshakeResponse(request: HttpRequest, response: HttpResponse) { } } @@ -147,14 +151,14 @@ abstract class AbstractWebsocketTest : RestServerTestBase() { performUnauthorizedTest(null) } - private fun performUnauthorizedTest(upgradeListener: UpgradeListener?) { + private fun performUnauthorizedTest(upgradeListener: JettyUpgradeListener?) { val wsClient = createWsClient() wsClient.start() val latch = CountDownLatch(2) var closeStatus: CloseStatus? = null - val wsHandler = object : NoOpEndpoint() { + val wsHandler = object : WebSocketAdapter() { override fun onWebSocketConnect(session: Session) { log.info("onWebSocketConnect : $session") diff --git a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerHTTPSWebsocketTest.kt b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerHTTPSWebsocketTest.kt index 20f28356189..f50e42982e5 100644 --- a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerHTTPSWebsocketTest.kt +++ b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerHTTPSWebsocketTest.kt @@ -8,6 +8,8 @@ import net.corda.rest.test.utils.TestHttpClientUnirestImpl import net.corda.rest.test.utils.multipartDir import net.corda.utilities.NetworkHostAndPort import org.eclipse.jetty.client.HttpClient +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic +import org.eclipse.jetty.io.ClientConnector import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.websocket.client.WebSocketClient import org.junit.jupiter.api.AfterAll @@ -53,6 +55,7 @@ class RestServerHTTPSWebsocketTest : AbstractWebsocketTest() { multipartDir, true ).apply { start() } + client = TestHttpClientUnirestImpl( "https://${restServerSettings.address.host}:${server.port}" + "/${restServerSettings.context.basePath}/${apiVersion.versionPath}/", @@ -70,7 +73,12 @@ class RestServerHTTPSWebsocketTest : AbstractWebsocketTest() { } } - override fun createWsClient() = WebSocketClient(HttpClient(SslContextFactory.Client(true))) + override fun createWsClient(): WebSocketClient { + val clientConnector = ClientConnector().apply { + sslContextFactory = SslContextFactory.Client(true) + } + return WebSocketClient(HttpClient(HttpClientTransportDynamic(clientConnector))) + } override val wsProtocol = "wss" diff --git a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerOpenApiTest.kt b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerOpenApiTest.kt index e5fd20f21b9..48021fcff07 100644 --- a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerOpenApiTest.kt +++ b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerOpenApiTest.kt @@ -496,7 +496,7 @@ class RestServerOpenApiTest : RestServerTestBase() { fun `GET swagger UI should return html with reference to swagger json`() { val apiSpec = client.call(GET, WebRequest("swagger")) assertEquals(HttpStatus.SC_OK, apiSpec.responseStatus) - assertEquals("text/html", apiSpec.headers["Content-Type"]) + assertEquals("text/html;charset=utf-8", apiSpec.headers["Content-Type"]) val expected = """url: "/${context.basePath}/${apiVersion.versionPath}/swagger.json"""" assertTrue(apiSpec.body!!.contains(expected)) } @@ -505,7 +505,7 @@ class RestServerOpenApiTest : RestServerTestBase() { fun `GET swagger UI with trailing slash in path should return html with reference to swagger json without trailing slash`() { val apiSpec = client.call(GET, WebRequest("swagger/")) assertEquals(HttpStatus.SC_OK, apiSpec.responseStatus) - assertEquals("text/html", apiSpec.headers["Content-Type"]) + assertEquals("text/html;charset=utf-8", apiSpec.headers["Content-Type"]) val expected = """url: "/${context.basePath}/${apiVersion.versionPath}/swagger.json"""" assertTrue(apiSpec.body!!.contains(expected)) } diff --git a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerRequestsTest.kt b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerRequestsTest.kt index 5b841ae4463..298c9a26a60 100644 --- a/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerRequestsTest.kt +++ b/libs/rest/rest-server-impl/src/integrationTest/kotlin/net/corda/rest/server/impl/RestServerRequestsTest.kt @@ -1,10 +1,10 @@ package net.corda.rest.server.impl import com.google.gson.Gson -import io.javalin.core.util.Header.ACCESS_CONTROL_ALLOW_CREDENTIALS -import io.javalin.core.util.Header.ACCESS_CONTROL_ALLOW_ORIGIN -import io.javalin.core.util.Header.CACHE_CONTROL -import io.javalin.core.util.Header.WWW_AUTHENTICATE +import io.javalin.http.Header.ACCESS_CONTROL_ALLOW_CREDENTIALS +import io.javalin.http.Header.ACCESS_CONTROL_ALLOW_ORIGIN +import io.javalin.http.Header.CACHE_CONTROL +import io.javalin.http.Header.WWW_AUTHENTICATE import net.corda.rest.annotations.RestApiVersion import net.corda.rest.server.apigen.test.TestJavaPrimitivesRestResourceImpl import net.corda.rest.server.config.models.RestServerSettings @@ -107,8 +107,9 @@ class RestServerRequestsTest : RestServerTestBase() { userName, password ) + assertEquals(HttpStatus.SC_OK, getPathResponse.responseStatus) - assertEquals("localhost", getPathResponse.headers[ACCESS_CONTROL_ALLOW_ORIGIN]) + assertEquals("http://localhost", getPathResponse.headers[ACCESS_CONTROL_ALLOW_ORIGIN]) assertEquals("true", getPathResponse.headers[ACCESS_CONTROL_ALLOW_CREDENTIALS]) assertEquals("no-cache", getPathResponse.headers[CACHE_CONTROL]) } diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientHttpRequestContext.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientHttpRequestContext.kt index 81d19e6a671..ff5951f6eec 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientHttpRequestContext.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientHttpRequestContext.kt @@ -1,10 +1,9 @@ package net.corda.rest.server.impl.context -import io.javalin.core.util.Header import io.javalin.http.Context +import io.javalin.http.Header import io.javalin.http.UploadedFile -import io.javalin.plugin.json.JsonMapper -import io.javalin.plugin.json.jsonMapper +import io.javalin.json.JsonMapper import net.corda.data.rest.PasswordExpiryStatus import net.corda.rest.server.impl.security.RestAuthenticationProvider @@ -14,7 +13,7 @@ import net.corda.rest.server.impl.security.RestAuthenticationProvider internal class ClientHttpRequestContext(private val ctx: Context) : ClientRequestContext { override val method: String - get() = ctx.method() + get() = ctx.method().name override fun header(header: String): String? = ctx.header(header) @@ -60,12 +59,12 @@ internal class ClientHttpRequestContext(private val ctx: Context) : ClientReques } override fun addPasswordExpiryHeader(expiryStatus: PasswordExpiryStatus) { - ctx.res.addHeader(Header.WARNING, "199 - PasswordExpiryStatus is $expiryStatus") + ctx.res().addHeader(Header.WARNING, "199 - PasswordExpiryStatus is $expiryStatus") } private fun addHeaderValues(values: Iterable) { values.forEach { - ctx.res.addHeader(Header.WWW_AUTHENTICATE, it) + ctx.res().addHeader(Header.WWW_AUTHENTICATE, it) } } } diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientRequestContext.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientRequestContext.kt index 2283962a36c..1addcff2ae1 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientRequestContext.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ClientRequestContext.kt @@ -1,10 +1,10 @@ package net.corda.rest.server.impl.context -import io.javalin.core.security.BasicAuthCredentials -import io.javalin.core.util.Header +import io.javalin.http.Header import io.javalin.http.UploadedFile -import io.javalin.http.util.ContextUtil -import io.javalin.plugin.json.JsonMapper +import io.javalin.http.servlet.getBasicAuthCredentials +import io.javalin.json.JsonMapper +import io.javalin.security.BasicAuthCredentials import net.corda.data.rest.PasswordExpiryStatus import net.corda.rest.server.impl.security.RestAuthenticationProvider @@ -101,7 +101,7 @@ interface ClientRequestContext { * Returns a Boolean which is true if there is an Authorization header with * Basic auth credentials. Returns false otherwise. */ - fun basicAuthCredentialsExist(): Boolean = ContextUtil.hasBasicAuthCredentials(header(Header.AUTHORIZATION)) + fun basicAuthCredentialsExist(): Boolean = getBasicAuthCredentials() != null /** * Gets basic-auth credentials from the request, or throws. @@ -109,5 +109,8 @@ interface ClientRequestContext { * Returns a wrapper object [BasicAuthCredentials] which contains the * Base64 decoded username and password from the Authorization header. */ - fun basicAuthCredentials(): BasicAuthCredentials = ContextUtil.getBasicAuthCredentials(header(Header.AUTHORIZATION)) + fun basicAuthCredentials(): BasicAuthCredentials = getBasicAuthCredentials()!! + + private fun getBasicAuthCredentials() = + getBasicAuthCredentials(header(Header.AUTHORIZATION)) } diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ContextUtils.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ContextUtils.kt index 5168e27df76..fdea461b529 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ContextUtils.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/ContextUtils.kt @@ -1,8 +1,8 @@ package net.corda.rest.server.impl.context -import io.javalin.core.util.Header import io.javalin.http.Context import io.javalin.http.ForbiddenResponse +import io.javalin.http.Header import io.javalin.http.UnauthorizedResponse import net.corda.data.rest.PasswordExpiryStatus import net.corda.metrics.CordaMetrics @@ -37,8 +37,6 @@ internal object ContextUtils { private val log = LoggerFactory.getLogger(ContextUtils::class.java) - const val contentTypeApplicationJson = "application/json" - private const val CORDA_X500_NAME = "O=HTTP REST Server, L=New York, C=US" private fun withMDC(user: String, method: String, path: String, block: () -> T): T { @@ -112,7 +110,7 @@ internal object ContextUtils { fun RouteInfo.invokeHttpMethod(): (Context) -> Unit { return { ctx -> - val ctxMethod = ctx.method() + val ctxMethod = ctx.method().name withMDC(restContext()?.principal ?: "", ctxMethod, ctx.path()) { val methodLogger = ctxMethod.loggerFor() methodLogger.info("Servicing $ctxMethod request to '${ctx.path()}' and invoking method \"${method.method.name}\"") @@ -189,9 +187,9 @@ internal object ContextUtils { ) private fun cleanUpMultipartRequest(ctx: Context) { - ctx.uploadedFiles().forEach { it.content.close() } + ctx.uploadedFiles().forEach { it.content().close() } // Remove all the parts and associated file storage once we are done with them - ctx.req.parts.forEach { part -> + ctx.req().parts.forEach { part -> try { part.delete() } catch (e: Exception) { diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/JsonResultBuilder.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/JsonResultBuilder.kt index ca66792fc68..f27f5198ed9 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/JsonResultBuilder.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/context/JsonResultBuilder.kt @@ -1,5 +1,6 @@ package net.corda.rest.server.impl.context +import io.javalin.http.ContentType import io.javalin.http.Context import net.corda.rest.ResponseCode import net.corda.rest.response.ResponseEntity @@ -14,11 +15,11 @@ fun Context.buildJsonResult(result: Any?, returnType: Class<*>) { // Add optional headers result.headers.forEach { - ctx.res.addHeader(it.key, it.value) + ctx.res().addHeader(it.key, it.value) } } (result as? String) != null -> - ctx.contentType(ContextUtils.contentTypeApplicationJson).result(result).status(ResponseCode.OK.statusCode) + ctx.contentType(ContentType.APPLICATION_JSON.mimeType).result(result).status(ResponseCode.OK.statusCode) result != null -> { // If the return type does not specify a response code (is not a HttpResponse) we default the status to 200 - OK. ctx.json(result).status(ResponseCode.OK.statusCode) diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/ParametersRetrieverContext.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/ParametersRetrieverContext.kt index a9c6ffacaaa..9f3eab80370 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/ParametersRetrieverContext.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/ParametersRetrieverContext.kt @@ -1,6 +1,5 @@ package net.corda.rest.server.impl.internal -import io.javalin.http.util.ContextUtil import net.corda.rest.HttpFileUpload import net.corda.rest.server.impl.context.ClientRequestContext import org.slf4j.LoggerFactory @@ -48,7 +47,8 @@ internal class ParametersRetrieverContext(private val ctx: ClientRequestContext) fun formParamMap(): Map> = ctx.formParamMap() fun pathParam(key: String): String { - return ContextUtil.pathParamOrThrow(pathParamsMap, key.lowercase(), ctx.matchedPath) + return pathParamsMap[key.lowercase()] + ?: throw IllegalArgumentException("Path parameter '$key' not found in path '${ctx.matchedPath}'") } fun queryParams(key: String): List = queryParamsMap[key.lowercase()] ?: emptyList() @@ -58,10 +58,26 @@ internal class ParametersRetrieverContext(private val ctx: ClientRequestContext) fun uploadedFiles(paramName: String): List { val files = ctx.uploadedFiles(paramName) - return files.map { file -> HttpFileUpload(file.content, file.contentType, file.extension, file.filename, file.size) } + return files.map { file -> + HttpFileUpload( + file.content(), + file.contentType(), + file.extension(), + file.filename(), + file.size() + ) + } } - fun fromJsonString(json: String, targetClass: Class): T { + fun fromJsonString(json: String, targetClass: Class): T? { + // The method fromJsonString does not accept null as a return value + // because the type accepted by the method must be a subclass of Any. + // In order for the function to be able to return null, the type should be a + // subclass of Any?. + // An attempt to return null results in a null exception. + if (json == "null") { + return null + } return ctx.jsonMapper.fromJsonString(json, targetClass) } } diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/RestServerInternal.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/RestServerInternal.kt index a308a909906..8a5e60e79ad 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/RestServerInternal.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/RestServerInternal.kt @@ -1,18 +1,20 @@ package net.corda.rest.server.impl.internal import io.javalin.Javalin -import io.javalin.core.util.Header +import io.javalin.config.JavalinConfig +import io.javalin.config.MultipartConfig +import io.javalin.config.SizeUnit import io.javalin.http.BadRequestResponse import io.javalin.http.ContentType import io.javalin.http.Context import io.javalin.http.HandlerType +import io.javalin.http.Header import io.javalin.http.HttpResponseException import io.javalin.http.NotFoundResponse import io.javalin.http.staticfiles.Location import io.javalin.http.util.JsonEscapeUtil -import io.javalin.http.util.MultipartUtil -import io.javalin.http.util.RedirectToLowercasePathPlugin -import io.javalin.plugin.json.JavalinJackson +import io.javalin.json.JavalinJackson +import io.javalin.plugin.bundled.RedirectToLowercasePathPlugin import net.corda.rest.authorization.AuthorizationUtils.authorize import net.corda.rest.server.config.RestServerSettingsProvider import net.corda.rest.server.impl.apigen.processing.RouteInfo @@ -20,7 +22,6 @@ import net.corda.rest.server.impl.apigen.processing.RouteProvider import net.corda.rest.server.impl.apigen.processing.openapi.OpenApiInfoProvider import net.corda.rest.server.impl.context.ClientHttpRequestContext import net.corda.rest.server.impl.context.ContextUtils.authenticate -import net.corda.rest.server.impl.context.ContextUtils.contentTypeApplicationJson import net.corda.rest.server.impl.context.ContextUtils.invokeHttpMethod import net.corda.rest.server.impl.context.ContextUtils.userNotAuthorized import net.corda.rest.server.impl.security.RestAuthenticationProvider @@ -34,10 +35,8 @@ import net.corda.utilities.debug import net.corda.utilities.trace import net.corda.web.server.JavalinStarter import org.eclipse.jetty.http2.HTTP2Cipher -import org.eclipse.jetty.server.HttpConfiguration import org.eclipse.jetty.server.HttpConnectionFactory import org.eclipse.jetty.server.SecureRequestCustomizer -import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.SslConnectionFactory import org.eclipse.jetty.util.ssl.SslContextFactory @@ -47,7 +46,6 @@ import org.osgi.framework.wiring.BundleWiring import org.slf4j.LoggerFactory import java.nio.file.Path import java.util.LinkedList -import javax.servlet.MultipartConfigElement @Suppress("TooManyFunctions", "TooGenericExceptionThrown", "LongParameterList") internal class RestServerInternal( @@ -78,10 +76,10 @@ internal class RestServerInternal( private lateinit var server: Javalin private val serverFactory: () -> Javalin = { - Javalin.create { - it.jsonMapper(JavalinJackson(serverJacksonObjectMapper)) - it.registerPlugin(RedirectToLowercasePathPlugin()) - configureJavalinForTracing(it) + Javalin.create { config -> + config.jsonMapper(JavalinJackson(serverJacksonObjectMapper)) + config.registerPlugin(RedirectToLowercasePathPlugin()) + configureJavalinForTracing(config) val swaggerUiBundle = getSwaggerUiBundle() // In an OSGi context, webjars cannot be loaded automatically using `JavalinConfig.enableWebJars`. @@ -91,53 +89,53 @@ internal class RestServerInternal( if (swaggerUiBundle != null) { val swaggerUiClassloader = swaggerUiBundle.adapt(BundleWiring::class.java).classLoader executeWithThreadContextClassLoader(swaggerUiClassloader) { - it.addStaticFiles("/META-INF/resources/", Location.CLASSPATH) + config.staticFiles.add("/META-INF/resources/", Location.CLASSPATH) } } else { - it.enableWebjars() + config.staticFiles.enableWebjars() } if (log.isDebugEnabled) { - it.enableDevLogging() + config.bundledPlugins.enableDevLogging() } - it.server { - configurationsProvider.getSSLKeyStorePath() - ?.let { createSecureServer() } - ?: INSECURE_SERVER_DEV_MODE_WARNING.let { msg -> - if (configurationsProvider.isDevModeEnabled()) { - log.warn(msg) - } else { - log.error(msg) - throw UnsupportedOperationException(msg) - } - createInsecureServer() - } + + if (configurationsProvider.getSSLKeyStorePath() != null) { + createSecureServer(config) + } else { + if (configurationsProvider.isDevModeEnabled()) { + log.warn(INSECURE_SERVER_DEV_MODE_WARNING) + } else { + log.error(INSECURE_SERVER_DEV_MODE_WARNING) + throw UnsupportedOperationException(INSECURE_SERVER_DEV_MODE_WARNING) + } + createInsecureServer(config) + } + + config.http.defaultContentType = ContentType.APPLICATION_JSON.mimeType + config.bundledPlugins.enableCors { cors -> + cors.addRule { rule -> + rule.reflectClientOrigin = true + rule.allowCredentials = true + } } - it.defaultContentType = contentTypeApplicationJson - it.enableCorsForAllOrigins() }.apply { addRoutes() addOpenApiRoutes() addWsRoutes() // In order for multipart content to be stored onto disk, we need to override some properties - // which are set by default by Javalin such that entire content is read into memory - MultipartUtil.preUploadFunction = { req -> - req.setAttribute( - "org.eclipse.jetty.multipartConfig", - MultipartConfigElement( - multiPartDir.toString(), - configurationsProvider.maxContentLength().toLong(), - configurationsProvider.maxContentLength().toLong(), - 1024 - ) - ) + // which are set by default by Javalin such that the entire content is read into memory + MultipartConfig().apply { + cacheDirectory(multiPartDir.toString()) + maxFileSize(configurationsProvider.maxContentLength().toLong(), SizeUnit.BYTES) + maxTotalRequestSize(configurationsProvider.maxContentLength().toLong(), SizeUnit.BYTES) + maxInMemoryFileSize(1024, SizeUnit.BYTES) } } } private fun addExceptionHandlers(app: Javalin) { app.exception(NotFoundResponse::class.java) { e, ctx -> - val detailsWithUrl = e.details.plus("url" to ctx.req.requestURL.toString()) + val detailsWithUrl = e.details.plus("url" to ctx.req().requestURL.toString()) commonResult(NotFoundResponse(details = detailsWithUrl), ctx) } app.exception(HttpResponseException::class.java) { e, ctx -> @@ -146,7 +144,7 @@ internal class RestServerInternal( } private fun commonResult(e: HttpResponseException, ctx: Context) { - if (ctx.header(Header.ACCEPT)?.contains(ContentType.JSON) == true || ctx.res.contentType == ContentType.JSON) { + if (ctx.header(Header.ACCEPT)?.contains(ContentType.JSON) == true || ctx.res().contentType == ContentType.JSON) { ctx.status(e.status).result( """{ | "title": "${e.message?.let { JsonEscapeUtil.escape(it) }}", @@ -202,7 +200,7 @@ internal class RestServerInternal( // "testEntity/getprotocolversion" and mistakenly finds "before" handler where there should be none. // Javalin provides no way for modifying "before" handler finding logic. if (resourceProvider.httpNoAuthRequiredGetRoutes.none { routeInfo -> routeInfo.fullPath == it.path() } && - it.method() == "GET" + it.method().name == "GET" ) { val clientHttpRequestContext = ClientHttpRequestContext(it) val authorizingSubject = authenticate(clientHttpRequestContext, restAuthProvider, credentialResolver) @@ -236,7 +234,7 @@ internal class RestServerInternal( private fun Javalin.registerHandlerForRoute(routeInfo: RouteInfo, handlerType: HandlerType) { try { - addHandler(handlerType, routeInfo.fullPath, routeInfo.invokeHttpMethod()) + addHttpHandler(handlerType, routeInfo.fullPath, routeInfo.invokeHttpMethod()) log.info("Added \"$handlerType\" handler for \"${routeInfo.fullPath}\".") } catch (e: Exception) { "Error during adding route. Handler type=$handlerType, Path=\"${routeInfo.fullPath}\"".let { @@ -252,7 +250,7 @@ internal class RestServerInternal( // For "before" handlers we have a global space of handlers in Javalin regardless of which method was actually // used. In case when two separate handlers created for GET and for DELETE for the same resource, without "if" // condition below both handlers will be used - which will be redundant. - if (it.method() == handlerType.name) { + if (it.method().name == handlerType.name) { with(configurationsProvider.maxContentLength()) { if (it.contentLength() > this) { throw BadRequestResponse( @@ -264,7 +262,8 @@ internal class RestServerInternal( } } val clientHttpRequestContext = ClientHttpRequestContext(it) - val authorizingSubject = authenticate(clientHttpRequestContext, restAuthProvider, credentialResolver) + val authorizingSubject = + authenticate(clientHttpRequestContext, restAuthProvider, credentialResolver) val authorizationProvider = routeInfo.method.instance.authorizationProvider val resourceAccessString = clientHttpRequestContext.getResourceAccessString() @@ -283,7 +282,7 @@ internal class RestServerInternal( openApiInfoProviders.forEach { openApiInfoProvider -> get( openApiInfoProvider.pathForOpenApiJson - ) { ctx -> ctx.result(openApiInfoProvider.openApiString).contentType(contentTypeApplicationJson) } + ) { ctx -> ctx.result(openApiInfoProvider.openApiString).contentType(ContentType.APPLICATION_JSON.mimeType) } get(openApiInfoProvider.pathForOpenApiUI, openApiInfoProvider.swaggerUIRenderer) } log.trace { "Add OpenApi route completed." } @@ -316,80 +315,54 @@ internal class RestServerInternal( } @SuppressWarnings("ComplexMethod") - private fun createSecureServer(): Server { + private fun createSecureServer(config: JavalinConfig) { log.trace { "Create secure (HTTPS) server." } require(configurationsProvider.getSSLKeyStorePath() != null) { "SSL key store path must be present in order to start a secure server" } require(configurationsProvider.getSSLKeyStorePassword() != null) { SSL_PASSWORD_MISSING } - log.trace { "Get SslContextFactory." } - val sslContextFactory = SslContextFactory.Server().apply { - keyStorePath = configurationsProvider.getSSLKeyStorePath()!!.toAbsolutePath().toString() - setKeyStorePassword(configurationsProvider.getSSLKeyStorePassword()!!) - cipherComparator = HTTP2Cipher.COMPARATOR - provider = "Conscrypt" - } - log.trace { "Get SslConnectionFactory." } - log.trace { "Get HttpConfiguration." } - val httpsConfig = HttpConfiguration().apply { - sendServerVersion = false - secureScheme = "https" - securePort = configurationsProvider.getHostAndPort().port - addCustomizer(SecureRequestCustomizer()) + config.jetty.modifyHttpConfiguration { httpConfig -> + httpConfig.sendServerVersion = false + httpConfig.secureScheme = "https" + httpConfig.securePort = configurationsProvider.getHostAndPort().port + httpConfig.addCustomizer( + SecureRequestCustomizer().apply { + isSniHostCheck = false + } + ) } - val http11 = HttpConnectionFactory(httpsConfig) + config.jetty.addConnector { server, httpConfig -> + log.trace { "Get SslContextFactory." } + val sslContextFactory = SslContextFactory.Server().apply { + keyStorePath = configurationsProvider.getSSLKeyStorePath()!!.toAbsolutePath().toString() + setKeyStorePassword(configurationsProvider.getSSLKeyStorePassword()!!) + cipherComparator = HTTP2Cipher.COMPARATOR + } - fun Server.addHttp11SslConnector() { + val http11 = HttpConnectionFactory(httpConfig) val ssl = SslConnectionFactory(sslContextFactory, http11.protocol) - addConnector( - ServerConnector(this, ssl, http11).apply { - port = configurationsProvider.getHostAndPort().port - host = configurationsProvider.getHostAndPort().host - } - ) - } - try { - return Server().apply { - /** - * HTTP2 connector currently disabled because: - * - There is no explicit requirement to support HTTP2 - * - There are not that many HTTP clients exist in Java that support HTTP2 - * - Fallback mechanism to HTTP 1.1 is not working as expected - */ - // addHttp2SslConnector() - // HTTP/1.1 Connector - addHttp11SslConnector() - }.also { log.trace { "Create secure (HTTPS) server completed." } } - } catch (e: Exception) { - "Error during Create secure (HTTPS) server".let { - log.error("$it: ${e.message}") - throw Exception(it, e) + ServerConnector(server, ssl, http11).apply { + port = configurationsProvider.getHostAndPort().port + host = configurationsProvider.getHostAndPort().host } } + log.trace { "Create secure (HTTPS) server completed." } } - private fun createInsecureServer(): Server { - try { - log.trace { "Create insecure (HTTP) server." } - - return Server().apply { - // HTTP/1.1 Connector - addConnector( - ServerConnector(this).apply { - port = configurationsProvider.getHostAndPort().port - host = configurationsProvider.getHostAndPort().host - } - ) - }.also { log.trace { "Create insecure (HTTP) server completed." } } - } catch (e: Exception) { - "Error during Create insecure (HTTP) server".let { - log.error("$it: ${e.message}") - throw Exception(it, e) + private fun createInsecureServer(config: JavalinConfig) { + log.trace { "Create insecure (HTTP) server." } + + config.jetty.addConnector { server, _ -> + ServerConnector(server).apply { + port = configurationsProvider.getHostAndPort().port + host = configurationsProvider.getHostAndPort().host } } + + log.trace { "Create insecure (HTTP) server." } } private fun Javalin.addWsRoutes() { diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/SwaggerUIRenderer.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/SwaggerUIRenderer.kt index 92e4cc5ff86..c3e4cc9624c 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/SwaggerUIRenderer.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/internal/SwaggerUIRenderer.kt @@ -1,9 +1,9 @@ package net.corda.rest.server.impl.internal -import io.javalin.core.util.Util import io.javalin.http.Context import io.javalin.http.Handler import io.javalin.http.InternalServerErrorResponse +import io.javalin.util.Util import net.corda.rest.server.config.RestServerSettingsProvider import net.corda.rest.server.impl.apigen.processing.openapi.OpenApiInfoProvider.Companion.jsonPath import org.osgi.framework.FrameworkUtil diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolver.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolver.kt index 37b7a153766..ec300458c40 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolver.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolver.kt @@ -1,6 +1,6 @@ package net.corda.rest.server.impl.security.provider.credentials -import io.javalin.core.util.Header.AUTHORIZATION +import io.javalin.http.Header.AUTHORIZATION import net.corda.rest.server.impl.context.ClientRequestContext import net.corda.rest.server.impl.security.provider.credentials.tokens.BearerTokenAuthenticationCredentials import net.corda.rest.server.impl.security.provider.credentials.tokens.UsernamePasswordAuthenticationCredentials diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/ServerDuplexChannel.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/ServerDuplexChannel.kt index d870ee53493..c9b9910a310 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/ServerDuplexChannel.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/ServerDuplexChannel.kt @@ -5,7 +5,6 @@ import net.corda.rest.ws.DuplexChannel import org.eclipse.jetty.websocket.api.CloseStatus import org.eclipse.jetty.websocket.api.StatusCode import java.lang.Exception -import java.util.concurrent.Future internal class ServerDuplexChannel( private val ctx: WsConnectContext, @@ -18,12 +17,12 @@ internal class ServerDuplexChannel( private var connectHook: (() -> Unit)? = null private var closeHook: ((statusCode: Int, reason: String?) -> Unit)? = null - override fun send(message: String): Future { - return ctx.send(message) + override fun send(message: String) { + ctx.send(message) } - override fun send(message: Any): Future { - return ctx.send(message) + override fun send(message: Any) { + ctx.send(message) } override fun close() { diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/WebSocketRouteAdaptor.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/WebSocketRouteAdaptor.kt index f745be89059..a8d6d6c7f2b 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/WebSocketRouteAdaptor.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/WebSocketRouteAdaptor.kt @@ -21,6 +21,7 @@ import net.corda.rest.ws.DuplexChannel import org.eclipse.jetty.websocket.api.CloseStatus import org.eclipse.jetty.websocket.api.StatusCode.POLICY_VIOLATION import org.slf4j.LoggerFactory +import java.time.Duration import java.util.concurrent.ConcurrentHashMap typealias SessionId = String @@ -51,16 +52,16 @@ internal class WebSocketRouteAdaptor( @Suppress("NestedBlockDepth") override fun handleConnect(ctx: WsConnectContext) { try { - channelsBySessionId[ctx.sessionId]?.let { - log.info("Session with id ${ctx.sessionId} already exists, overwriting and closing the old session.") - it.close("New session overwriting old session with id ${ctx.sessionId}") + channelsBySessionId[ctx.sessionId()]?.let { + log.info("Session with id ${ctx.sessionId()} already exists, overwriting and closing the old session.") + it.close("New session overwriting old session with id ${ctx.sessionId()}") } log.info("Connected to remote: ${ctx.session.remoteAddress}") - ServerDuplexChannel(ctx, webSocketCloserService, ctx.sessionId).let { newChannel -> - channelsBySessionId[ctx.sessionId] = newChannel + ServerDuplexChannel(ctx, webSocketCloserService, ctx.sessionId()).let { newChannel -> + channelsBySessionId[ctx.sessionId()] = newChannel - ctx.session.idleTimeout = webSocketIdleTimeoutMs + ctx.session.idleTimeout = Duration.ofMillis(webSocketIdleTimeoutMs) val clientWsRequestContext = ClientWsRequestContext(ctx) try { @@ -95,7 +96,7 @@ internal class WebSocketRouteAdaptor( // incoming messages could be malicious. We won't do anything with the message unless an onTextMessage // hook has been defined. The hook will be responsible for ensuring the messages respect the protocol // and terminate connections when malicious messages arrive. - requireNotNull(channelsBySessionId[ctx.sessionId]).onTextMessage?.invoke(ctx.message()) + requireNotNull(channelsBySessionId[ctx.sessionId()]).onTextMessage?.invoke(ctx.message()) ?: log.info("Inbound messages are not supported.") } catch (th: Throwable) { log.error("Exception during message handling", th) @@ -105,7 +106,7 @@ internal class WebSocketRouteAdaptor( // The handler is called when an error is detected. override fun handleError(ctx: WsErrorContext) { try { - requireNotNull(channelsBySessionId[ctx.sessionId]).onError?.invoke(ctx.error()) + requireNotNull(channelsBySessionId[ctx.sessionId()]).onError?.invoke(ctx.error()) } catch (th: Throwable) { log.error("Unexpected exception in handleError", th) } @@ -114,7 +115,7 @@ internal class WebSocketRouteAdaptor( // The handler is called when a WebSocket client closes the connection. override fun handleClose(ctx: WsCloseContext) { try { - channelsBySessionId.remove(ctx.sessionId)?.onClose?.invoke(ctx.status(), ctx.reason()) + channelsBySessionId.remove(ctx.sessionId())?.onClose?.invoke(ctx.status(), ctx.reason()) } catch (th: Throwable) { log.error("Unexpected exception in handleClose", th) } diff --git a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/deferred/DeferredWebSocketCloserService.kt b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/deferred/DeferredWebSocketCloserService.kt index 8d7aa07f02f..3394f10ca61 100644 --- a/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/deferred/DeferredWebSocketCloserService.kt +++ b/libs/rest/rest-server-impl/src/main/kotlin/net/corda/rest/server/impl/websocket/deferred/DeferredWebSocketCloserService.kt @@ -25,10 +25,10 @@ class DeferredWebSocketCloserService : WebSocketCloserService { override fun close(webSocketContext: WsContext, closeStatus: CloseStatus) { deferredWebsocketClosePool.schedule({ if (webSocketContext.session.isOpen) { - log.info("Closing open session ${webSocketContext.sessionId}: status ${closeStatus.code}, reason: ${closeStatus.phrase}") + log.info("Closing open session ${webSocketContext.sessionId()}: status ${closeStatus.code}, reason: ${closeStatus.phrase}") } else { log.info( - "Closing session ${webSocketContext.sessionId} that's already reported closed: " + + "Closing session ${webSocketContext.sessionId()} that's already reported closed: " + "status ${closeStatus.code}, reason: ${closeStatus.phrase}" ) } diff --git a/libs/rest/rest-server-impl/src/test/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolverTest.kt b/libs/rest/rest-server-impl/src/test/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolverTest.kt index 8578e4e8fe5..a555bc5f55e 100644 --- a/libs/rest/rest-server-impl/src/test/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolverTest.kt +++ b/libs/rest/rest-server-impl/src/test/kotlin/net/corda/rest/server/impl/security/provider/credentials/DefaultCredentialResolverTest.kt @@ -1,7 +1,9 @@ package net.corda.rest.server.impl.security.provider.credentials -import io.javalin.core.util.Header.AUTHORIZATION import io.javalin.http.Context +import io.javalin.http.Header.AUTHORIZATION +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import net.corda.rest.server.impl.context.ClientHttpRequestContext import net.corda.rest.server.impl.security.provider.credentials.tokens.BearerTokenAuthenticationCredentials import net.corda.rest.server.impl.security.provider.credentials.tokens.UsernamePasswordAuthenticationCredentials @@ -9,19 +11,23 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.Base64 import javax.security.auth.login.FailedLoginException -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse class DefaultCredentialResolverTest { // need to mock these as Javalin's context class in unmockable due to overload resolution issues // https://github.com/mockito/mockito/issues/1943 private val req: HttpServletRequest = mock() private val res: HttpServletResponse = mock() - private val context = ClientHttpRequestContext(Context(req, res, emptyMap())) + private val ctx = mock().apply { + whenever(req()).thenReturn(req) + whenever(res()).thenReturn(res) + whenever(header(any())).thenCallRealMethod() + } + private val context = ClientHttpRequestContext(ctx) private val resolver = DefaultCredentialResolver() @Test diff --git a/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/TestHealthCheckAPIImpl.kt b/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/TestHealthCheckAPIImpl.kt index eee5bfd5994..1650977756e 100644 --- a/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/TestHealthCheckAPIImpl.kt +++ b/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/TestHealthCheckAPIImpl.kt @@ -118,11 +118,9 @@ class TestHealthCheckAPIImpl : TestHealthCheckAPI, PluggableRestResource= start + range) { - // Wait for sent confirmation then close the channel - future.get() channel.close() } } diff --git a/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/utils/TestHttpClient.kt b/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/utils/TestHttpClient.kt index 976d13c63bc..42f906f44a4 100644 --- a/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/utils/TestHttpClient.kt +++ b/libs/rest/rest-test-common/src/main/kotlin/net/corda/rest/test/utils/TestHttpClient.kt @@ -65,7 +65,7 @@ class TestHttpClientUnirestImpl(override val baseAddress: String, private val en ) } - private fun HttpRequest<*>.addOriginHeader() = header("Origin", "localhost") + private fun HttpRequest<*>.addOriginHeader() = header("Origin", "http://localhost") override fun call(verb: HttpVerb, webRequest: WebRequest, userName: String, password: String): WebResponse { return doCall(verb, webRequest) { @@ -87,7 +87,7 @@ class TestHttpClientUnirestImpl(override val baseAddress: String, private val en }.addOriginHeader() request.encodeAuth() - + request.responseEncoding("utf-8") request = if (isMultipartFormRequest(webRequest)) { buildMultipartFormRequest(webRequest, request) } else { diff --git a/libs/rest/rest/src/main/kotlin/net/corda/rest/ws/DuplexChannel.kt b/libs/rest/rest/src/main/kotlin/net/corda/rest/ws/DuplexChannel.kt index 815a596c398..da1d38110d1 100644 --- a/libs/rest/rest/src/main/kotlin/net/corda/rest/ws/DuplexChannel.kt +++ b/libs/rest/rest/src/main/kotlin/net/corda/rest/ws/DuplexChannel.kt @@ -1,7 +1,6 @@ package net.corda.rest.ws import java.lang.Exception -import java.util.concurrent.Future /** * Channel to facilitate full duplex (i.e. two-way communication) like WebSockets protocol. @@ -16,14 +15,14 @@ interface DuplexChannel : AutoCloseable { val id: String /** - * Allows to asynchronously send a message to the remote side + * Allows to synchronously send a message to the remote side */ - fun send(message: String): Future + fun send(message: String) /** - * Allows to asynchronously send a message to the remote side + * Allows to synchronously send a message to the remote side */ - fun send(message: Any): Future + fun send(message: Any) /** * Allows to close this communication channel diff --git a/libs/tracing-impl/src/main/kotlin/net/corda/tracing/brave/BraveTracingService.kt b/libs/tracing-impl/src/main/kotlin/net/corda/tracing/brave/BraveTracingService.kt index 883caa5569e..7088c21d96c 100644 --- a/libs/tracing-impl/src/main/kotlin/net/corda/tracing/brave/BraveTracingService.kt +++ b/libs/tracing-impl/src/main/kotlin/net/corda/tracing/brave/BraveTracingService.kt @@ -13,13 +13,14 @@ import brave.http.HttpRequestMatchers.pathStartsWith import brave.http.HttpRequestParser import brave.http.HttpRuleSampler import brave.http.HttpTracing +import brave.jakarta.servlet.TracingFilter import brave.propagation.B3Propagation import brave.propagation.ThreadLocalCurrentTraceContext import brave.sampler.RateLimitingSampler import brave.sampler.Sampler import brave.sampler.SamplerFunction -import brave.servlet.TracingFilter -import io.javalin.core.JavalinConfig +import io.javalin.config.JavalinConfig +import jakarta.servlet.DispatcherType import net.corda.messaging.api.records.EventLogRecord import net.corda.messaging.api.records.Record import net.corda.tracing.BatchPublishTracing @@ -40,7 +41,6 @@ import java.util.Stack import java.util.concurrent.ExecutorService import java.util.logging.Level import java.util.logging.Logger -import javax.servlet.DispatcherType internal sealed interface SampleRate internal object Unlimited : SampleRate @@ -267,7 +267,7 @@ internal class BraveTracingService( } override fun configureJavalin(config: Any) { - (config as JavalinConfig).configureServletContextHandler { sch -> + (config as JavalinConfig).jetty.modifyServletContextHandler() { sch -> sch.addFilter( FilterHolder(TracingFilter.create(httpTracing)), "/*", diff --git a/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinServer.kt b/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinServer.kt index be9a1ee2f25..da7d03f22de 100644 --- a/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinServer.kt +++ b/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinServer.kt @@ -42,10 +42,10 @@ class JavalinServer( Javalin.create { config -> // hardcode to 100Mb for now // TODO CORE-17986: make configurable - config.maxRequestSize = maxRequestSize + config.http.maxRequestSize = maxRequestSize if (log.isDebugEnabled) { - config.enableDevLogging() + config.bundledPlugins.enableDevLogging() } configureJavalinForTracing(config) } @@ -87,7 +87,7 @@ class JavalinServer( } server?.exception(NotFoundResponse::class.java) { _, ctx -> - log.warn("Received request on non-existing endpoint: ${ctx.req.requestURI}") + log.warn("Received request on non-existing endpoint: ${ctx.req().requestURI}") ctx.result("404 Not Found") ctx.status(404) } diff --git a/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinStarter.kt b/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinStarter.kt index eba576000d1..bda4d9ee99e 100644 --- a/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinStarter.kt +++ b/libs/web/web-impl/src/main/kotlin/net/corda/web/server/JavalinStarter.kt @@ -5,7 +5,7 @@ import net.corda.utilities.classload.OsgiClassLoader import net.corda.utilities.classload.executeWithThreadContextClassLoader import net.corda.utilities.executeWithStdErrSuppressed import net.corda.utilities.trace -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory +import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory import org.osgi.framework.Bundle import org.osgi.framework.FrameworkUtil import org.slf4j.LoggerFactory @@ -46,7 +46,7 @@ object JavalinStarter { try { logger.trace { "Starting the $name Javalin server." } - val bundle = FrameworkUtil.getBundle(WebSocketServletFactory::class.java) + val bundle = FrameworkUtil.getBundle(JettyWebSocketServletFactory::class.java) if (bundle != null) { val bundleList = threadLocalClassLoaderBundles.plus(bundle) val osgiClassLoader = OsgiClassLoader(bundleList) diff --git a/tools/corda-runtime-gradle-plugin/src/integrationTest/kotlin/net/corda/gradle/plugin/FunctionalBaseTest.kt b/tools/corda-runtime-gradle-plugin/src/integrationTest/kotlin/net/corda/gradle/plugin/FunctionalBaseTest.kt index 5ff1c5c22e8..28d967afdce 100644 --- a/tools/corda-runtime-gradle-plugin/src/integrationTest/kotlin/net/corda/gradle/plugin/FunctionalBaseTest.kt +++ b/tools/corda-runtime-gradle-plugin/src/integrationTest/kotlin/net/corda/gradle/plugin/FunctionalBaseTest.kt @@ -1,6 +1,7 @@ package net.corda.gradle.plugin import io.javalin.Javalin +import io.javalin.config.JavalinConfig import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.BuildTask import org.gradle.testkit.runner.GradleRunner @@ -12,7 +13,7 @@ import org.junit.jupiter.api.io.TempDir import java.io.File // https://docs.gradle.org/current/userguide/test_kit.html -abstract class FunctionalBaseTest : Javalin() { +abstract class FunctionalBaseTest : Javalin(JavalinConfig()) { @field:TempDir lateinit var projectDir: File protected lateinit var buildFile: File