From de028846f12d1de7111dbb0eb24243f6b59c18a4 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Fri, 9 Jun 2023 18:07:33 +0200 Subject: [PATCH] http/2 is functional --- .../main/config/pairgoth.default.properties | 2 +- webserver/pom.xml | 21 ++++ .../jeudego/pairgoth/application/Pairgoth.kt | 106 +++++++++++++++--- .../main/resources/server.default.properties | 3 + .../src/main/resources/ssl/localhost.crt | 25 +++++ .../src/main/resources/ssl/localhost.key | 28 +++++ 6 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 webserver/src/main/resources/server.default.properties create mode 100644 webserver/src/main/resources/ssl/localhost.crt create mode 100644 webserver/src/main/resources/ssl/localhost.key diff --git a/view-webapp/src/main/config/pairgoth.default.properties b/view-webapp/src/main/config/pairgoth.default.properties index b0ebec3..48e854b 100644 --- a/view-webapp/src/main/config/pairgoth.default.properties +++ b/view-webapp/src/main/config/pairgoth.default.properties @@ -1,6 +1,6 @@ # webapp webapp.env = dev -webapp.url = http://localhost:8080 +webapp.url = https://localhost:8080 # store store = file diff --git a/webserver/pom.xml b/webserver/pom.xml index cb76603..39afe2e 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -46,6 +46,21 @@ jetty-jndi ${jetty.version} + + org.eclipse.jetty + jetty-alpn-server + ${jetty.version} + + + org.eclipse.jetty + jetty-alpn-java-server + ${jetty.version} + + + org.eclipse.jetty.http2 + http2-server + ${jetty.version} + commons-io commons-io @@ -127,11 +142,17 @@ + org.apache.maven.plugins maven-surefire-plugin + org.apache.maven.plugins maven-failsafe-plugin + + org.apache.maven.plugins + maven-resources-plugin + org.apache.maven.plugins maven-shade-plugin diff --git a/webserver/src/main/kotlin/org/jeudego/pairgoth/application/Pairgoth.kt b/webserver/src/main/kotlin/org/jeudego/pairgoth/application/Pairgoth.kt index ebd9781..979f8f9 100644 --- a/webserver/src/main/kotlin/org/jeudego/pairgoth/application/Pairgoth.kt +++ b/webserver/src/main/kotlin/org/jeudego/pairgoth/application/Pairgoth.kt @@ -1,16 +1,37 @@ package org.jeudego.pairgoth.application import org.apache.commons.io.FileUtils +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory +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.server.handler.ContextHandlerCollection +import org.eclipse.jetty.util.resource.Resource +import org.eclipse.jetty.util.ssl.SslContextFactory import org.eclipse.jetty.webapp.WebAppContext +import java.io.ByteArrayInputStream import java.io.File import java.io.FileReader +import java.io.InputStreamReader import java.net.JarURLConnection +import java.net.URL +import java.net.URLDecoder +import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path +import java.security.KeyFactory +import java.security.KeyStore +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.spec.PKCS8EncodedKeySpec import java.util.* import java.util.jar.JarFile +import java.util.regex.Pattern + fun main(vararg args: String) { try { @@ -32,9 +53,9 @@ private fun extractWarFiles() { Files.createDirectories(targetPath) // extract wars - val webappsFolderURL = object{}::class.java.enclosingClass.getResource("/META-INF/webapps") ?: throw Error("webapps not found") + val webappsFolderURL = getResource("/META-INF/webapps") ?: throw Error("webapps not found") val jarConnection = webappsFolderURL.openConnection() as JarURLConnection - val jarFile: JarFile = jarConnection.getJarFile() + val jarFile: JarFile = jarConnection.jarFile jarFile.entries().toList().filter { entry -> entry.name.startsWith(jarConnection.entryName) }.forEach { entry -> @@ -46,36 +67,89 @@ private fun extractWarFiles() { } } +private val mainClass = object{}::class.java.enclosingClass +private val jarPath = mainClass.protectionDomain.codeSource.location.path.let { URLDecoder.decode(it, "UTF-8") } +private val serverProps = Properties() +private fun getResource(resource: String) = mainClass.getResource(resource) +private fun getResourceProperty(key: String) = serverProps.getProperty(key)?.let { property -> + val url = property.replace("\$jar", jarPath) + if (!Resource.newResource(url).exists()) throw Error("resource not found: $url") + URL(url) +} ?: throw Error("missing property: $key") + private fun launchServer() { - // create server - val server = Server(8080) // CB TODO port is to be calculated from webapp.url - // create webapps contexts - val apiContext = createContext("api", "/api"); - val viewContext = createContext("view", "/"); + val apiContext = createContext("api", "/api") + val viewContext = createContext("view", "/") // handle properties - val properties = File("./pairgoth.properties"); + val defaultProps = getResource("/server.default.properties") ?: throw Error("missing default server properties") + defaultProps.openStream().use { + serverProps.load(InputStreamReader(it, StandardCharsets.UTF_8)) + } + val properties = File("./pairgoth.properties") if (properties.exists()) { - val props = Properties() - props.load(FileReader(properties)); - props.entries.forEach { entry -> + serverProps.load(FileReader(properties)) + serverProps.entries.forEach { entry -> val property = entry.key as String val value = entry.value as String if (property.startsWith("logger.")) { // special handling for logger properties val webappLoggerPropKey = "webapp-slf4j-logger.${property.substring(7)}" - apiContext.setInitParameter(webappLoggerPropKey, value); - viewContext.setInitParameter(webappLoggerPropKey, value); + apiContext.setInitParameter(webappLoggerPropKey, value) + viewContext.setInitParameter(webappLoggerPropKey, value) + } else if (property.startsWith("webapp.ssl.")) { + // do not propagate ssl properties further } else { - System.setProperty("pairgoth.$property", value); + System.setProperty("pairgoth.$property", value) } } } + // create server + val server = Server(8080) // CB TODO port is to be calculated from webapp.url + // register webapps - server.handler = ContextHandlerCollection(apiContext, viewContext); + server.handler = ContextHandlerCollection(apiContext, viewContext) + + // set up http/2 + val httpConfig = HttpConfiguration().apply { + addCustomizer(SecureRequestCustomizer()) + } + val http11 = HttpConnectionFactory(httpConfig) + val h2 = HTTP2ServerConnectionFactory(httpConfig) + val alpn = ALPNServerConnectionFactory().apply { + defaultProtocol = http11.protocol + } + val cert = getResourceProperty("webapp.ssl.cert").readBytes() + val key = getResourceProperty("webapp.ssl.key").readText().let { + val encodedKey = Pattern.compile("(?m)(?s)^---*BEGIN.*---*$(.*)^---*END.*---*$.*").matcher(it).replaceFirst("$1") + Base64.getDecoder().decode(encodedKey.replace("\n", "")) + } + val pass = serverProps.getProperty("webapp.ssl.pass") ?: "foobar" + + val keyFactory = KeyFactory.getInstance("RSA") + val keySpec = PKCS8EncodedKeySpec(key) + val privKey = keyFactory.generatePrivate(keySpec) + + val certificateFactory = CertificateFactory.getInstance("X.509") + val store = KeyStore.getInstance("JKS").apply { + load(null) + setCertificateEntry("certificate", certificateFactory.generateCertificate(ByteArrayInputStream(cert)) as X509Certificate) + setKeyEntry("key", privKey, pass.toCharArray(), arrayOf(certificateFactory.generateCertificate(ByteArrayInputStream(cert)))) + } + val sslContextFactory = SslContextFactory.Server().apply { + keyStoreType = "JKS" + keyStore = store + keyStorePassword = pass + // if (pass.isNotEmpty()) keyManagerPassword = pass + } + + val tls = SslConnectionFactory(sslContextFactory, alpn.protocol) + val connector = ServerConnector(server, tls, alpn, h2, http11) + connector.port = 8443 + server.addConnector(connector) // launch server server.start() @@ -84,5 +158,5 @@ private fun launchServer() { private fun createContext(webapp: String, contextPath: String) = WebAppContext().also { context -> context.war = "$tmp/pairgoth/webapps/$webapp-webapp-$version.war" - context.contextPath = contextPath; + context.contextPath = contextPath } diff --git a/webserver/src/main/resources/server.default.properties b/webserver/src/main/resources/server.default.properties new file mode 100644 index 0000000..72fc84e --- /dev/null +++ b/webserver/src/main/resources/server.default.properties @@ -0,0 +1,3 @@ +webapp.ssl.key = jar:file:$jar!/ssl/localhost.key +# webapp.ssl.pass = foobar (not supported for now) +webapp.ssl.cert = jar:file:$jar!/ssl/localhost.crt diff --git a/webserver/src/main/resources/ssl/localhost.crt b/webserver/src/main/resources/ssl/localhost.crt new file mode 100644 index 0000000..79cb1a2 --- /dev/null +++ b/webserver/src/main/resources/ssl/localhost.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIELTCCAxWgAwIBAgIUOOB8QYucWNOJYAg1ypawCJpdE9kwDQYJKoZIhvcNAQEL +BQAwgaUxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJZEYxDjAMBgNVBAcMBVBhcmlz +MSwwKgYDVQQKDCNGw4PCqWTDg8KpcmF0aW9uIEZyYW7Dg8KnYWlzZSBkZSBHbzER +MA8GA1UECwwIcGFpcmdvdGgxEjAQBgNVBAMMCWxvY2FsaG9zdDEjMCEGCSqGSIb3 +DQEJARYUcGFpcmdvdGhAamV1ZGVnby5vcmcwHhcNMjMwNjA5MTMwMDMyWhcNMzMw +NjA2MTMwMDMyWjCBpTELMAkGA1UEBhMCRlIxDDAKBgNVBAgMA0lkRjEOMAwGA1UE +BwwFUGFyaXMxLDAqBgNVBAoMI0bDg8KpZMODwqlyYXRpb24gRnJhbsODwqdhaXNl +IGRlIEdvMREwDwYDVQQLDAhwYWlyZ290aDESMBAGA1UEAwwJbG9jYWxob3N0MSMw +IQYJKoZIhvcNAQkBFhRwYWlyZ290aEBqZXVkZWdvLm9yZzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAJXPkr0CZf4xneSXFIUUWn1HNlgEVWEbhUbnrzqA +DqGN/MkpUI6/viXurNg1yLRPLeRvmlSUhwPc6Sq5HGZqimKFXcU/JNp+H4yiQluo +ykOvHTGhkYUtXJK91oxrghqU2kEOfJeoOyCpcBvPzBP0a3iSDBUTgazNiZWKvZ7L +MwYDj0sfC0d8zXlKxpsg62G9AIxOJEol5l7BrxtlUdO9xS44s6vrhlXYkdOdt5gV +77iQOWWiFJyBuhFhKrtUXP5yVPGVVko7HC6vtaiVRbzdh4g0j2tI4sq88tcTWpXA +iIrbmSCCDNkW0XfMap/zgL9Tkqb5kEbdPPuDitZftt9zsMsCAwEAAaNTMFEwHQYD +VR0OBBYEFDR7lP45ae2rgeyPiaIUk9AOd6geMB8GA1UdIwQYMBaAFDR7lP45ae2r +geyPiaIUk9AOd6geMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +ACp3YHugHxuoktyEB/pSEhkXFAYhaUYty6ESb0hQzF4CK9xY0fdabC40RFjGhGlg +STBez30thKbmcYoCUy0VG9lf8qndF0xgjihXzBHmRd1b4PO8AkAeMIbAKGhFeptf +MKIdfBjmrYAUA0L6GMU/h5hWPOzf6KACiJ8nHNjgYFsTrHO52QGqSqgy4j1l6TtQ +hlM3rxK0gSWzafZW/syxDXeM/Rocq0R48E90WpO9A4E9dg2PQtYT2zk7MMuMMIRg +D317F3FSFWI6XIg6EyLVx69rfuKiMqZ4ghTyXKyKcSC+LJQJhKFtOiLAdXaJ3naF +I7MPu1wZ/ZGRd4AvjsF/5jc= +-----END CERTIFICATE----- diff --git a/webserver/src/main/resources/ssl/localhost.key b/webserver/src/main/resources/ssl/localhost.key new file mode 100644 index 0000000..28d4b64 --- /dev/null +++ b/webserver/src/main/resources/ssl/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVz5K9AmX+MZ3k +lxSFFFp9RzZYBFVhG4VG5686gA6hjfzJKVCOv74l7qzYNci0Ty3kb5pUlIcD3Okq +uRxmaopihV3FPyTafh+MokJbqMpDrx0xoZGFLVySvdaMa4IalNpBDnyXqDsgqXAb +z8wT9Gt4kgwVE4GszYmVir2eyzMGA49LHwtHfM15SsabIOthvQCMTiRKJeZewa8b +ZVHTvcUuOLOr64ZV2JHTnbeYFe+4kDllohScgboRYSq7VFz+clTxlVZKOxwur7Wo +lUW83YeINI9rSOLKvPLXE1qVwIiK25kgggzZFtF3zGqf84C/U5Km+ZBG3Tz7g4rW +X7bfc7DLAgMBAAECggEAAtwIgUOdY5HJA55h1AVCXoNluq9LjrUWcAgJayS1AKdc +XKqa0OdBGzrqPwQfQ4to9iydTT92ZEh7/QJgU925ijhpd+1NbGFulP/RvYDQ4fDJ +NqssbcWJOCcG8zjOYRL6BWE1aKx1HQAWau03ZiKkeoKNAp9PoQHU8WaWaqaWzrwm +XZA2K1yb8jKdEGEqvyvt8bOIwAgerdWwNQPe6oqQKeCH2471YRKtIsAdVRqQwxzB +GmKyNcc8AaG4hJAZGu59HLqAwBdLht/v2ZU5QFBeRq4ltzxe9wPlW08dHu1D3l4M +rdIRrWn1FFVFjs71fgb+Abs9Fpi/+F3JNEyoxnz8YQKBgQDR5C4VFR4YczCFOXaB +JAq7aF2SCLggotRO33O/Nbdl7+qJxVjl80DcbLhlgYQIZC4osUoF7JPfXPJVrYX7 +plEy/071YHikuLpBcd0FBQpnmDi0BQWbbBMKVwQCH53Lrz4FuI+3jbEozKi7BIvN +WMzAt6fIXyPyPMI7eGWiUcUcZQKBgQC2uJmlTGDQOhPbl2ZS0Iya3hyZ0iQXI25L +iyrHYvKN7AgQnYYn4eYrJoRh/gh9cWDSEUSB9OLY8ekEVmzmUWQVjgycaYE4seFO +B4NWc2L+aALszES9C3SRupMa3jVgDPBI4u/DCWJrg9ttSPMEgwYFk33mFxlAQc53 +0LBn14pNbwKBgBVYeFtKh4IDDPcvjd66VKEUjxeP7XHcPW08CmByzRD/4kFaoZzZ +LUp9gA9KqavUzGD1DsslcTBxGnAeMpcSJgXisxv/UKWn58FKHCkrhxBcCcA9FoHk +7tbJXK3+mySg0NTyHSOUtGSq06oZX0Jl+oTK6LRXAKfdB//WUbe9SyeFAoGAE8dL +ql7oI+IFgEGVK+WzMphUVDow+eg16it4R/jn9IDWJqZGfU6wkX8r2UecN6fsKREB +b2fInl8hL/0C8LNiuAqWRuAMwsxObRnXF6aJ0qwDlQpPbn8s8RFXFxNyh6Ee6WTX +Oy9q3eR5/gxlcdmU70mV2TAq5Y+5/7IxRixIpjUCgYEAlMK/pFAnmaUqgl5UUyA5 +vDGgCoPb3ZBTF8yNndaOq07cDmGPIURwQKiI24iAm2HNyoEP5gVEvZ8jY2DAgqPt +zsv6TaG1w112HA+l1tJ8cfQTMm7COwXFjdPJ0lOb8VaNoAzeW+/B7+IsVCa2yjP4 +LZWxz7cjuLlYfZk3KM06HmA= +-----END PRIVATE KEY-----