http/2 is functional

This commit is contained in:
Claude Brisson
2023-06-09 18:07:33 +02:00
parent 5968bf97b3
commit de028846f1
6 changed files with 168 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
# webapp # webapp
webapp.env = dev webapp.env = dev
webapp.url = http://localhost:8080 webapp.url = https://localhost:8080
# store # store
store = file store = file

View File

@@ -46,6 +46,21 @@
<artifactId>jetty-jndi</artifactId> <artifactId>jetty-jndi</artifactId>
<version>${jetty.version}</version> <version>${jetty.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-alpn-java-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.http2</groupId>
<artifactId>http2-server</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
@@ -127,11 +142,17 @@
</executions> </executions>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId> <artifactId>maven-failsafe-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>

View File

@@ -1,16 +1,37 @@
package org.jeudego.pairgoth.application package org.jeudego.pairgoth.application
import org.apache.commons.io.FileUtils 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.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.server.SslConnectionFactory
import org.eclipse.jetty.server.handler.ContextHandlerCollection 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 org.eclipse.jetty.webapp.WebAppContext
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.io.FileReader import java.io.FileReader
import java.io.InputStreamReader
import java.net.JarURLConnection 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.Files
import java.nio.file.Path 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.*
import java.util.jar.JarFile import java.util.jar.JarFile
import java.util.regex.Pattern
fun main(vararg args: String) { fun main(vararg args: String) {
try { try {
@@ -32,9 +53,9 @@ private fun extractWarFiles() {
Files.createDirectories(targetPath) Files.createDirectories(targetPath)
// extract wars // 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 jarConnection = webappsFolderURL.openConnection() as JarURLConnection
val jarFile: JarFile = jarConnection.getJarFile() val jarFile: JarFile = jarConnection.jarFile
jarFile.entries().toList().filter { entry -> jarFile.entries().toList().filter { entry ->
entry.name.startsWith(jarConnection.entryName) entry.name.startsWith(jarConnection.entryName)
}.forEach { entry -> }.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() { private fun launchServer() {
// create server
val server = Server(8080) // CB TODO port is to be calculated from webapp.url
// create webapps contexts // create webapps contexts
val apiContext = createContext("api", "/api"); val apiContext = createContext("api", "/api")
val viewContext = createContext("view", "/"); val viewContext = createContext("view", "/")
// handle properties // 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()) { if (properties.exists()) {
val props = Properties() serverProps.load(FileReader(properties))
props.load(FileReader(properties)); serverProps.entries.forEach { entry ->
props.entries.forEach { entry ->
val property = entry.key as String val property = entry.key as String
val value = entry.value as String val value = entry.value as String
if (property.startsWith("logger.")) { if (property.startsWith("logger.")) {
// special handling for logger properties // special handling for logger properties
val webappLoggerPropKey = "webapp-slf4j-logger.${property.substring(7)}" val webappLoggerPropKey = "webapp-slf4j-logger.${property.substring(7)}"
apiContext.setInitParameter(webappLoggerPropKey, value); apiContext.setInitParameter(webappLoggerPropKey, value)
viewContext.setInitParameter(webappLoggerPropKey, value); viewContext.setInitParameter(webappLoggerPropKey, value)
} else if (property.startsWith("webapp.ssl.")) {
// do not propagate ssl properties further
} else { } 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 // 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 // launch server
server.start() server.start()
@@ -84,5 +158,5 @@ private fun launchServer() {
private fun createContext(webapp: String, contextPath: String) = WebAppContext().also { context -> private fun createContext(webapp: String, contextPath: String) = WebAppContext().also { context ->
context.war = "$tmp/pairgoth/webapps/$webapp-webapp-$version.war" context.war = "$tmp/pairgoth/webapps/$webapp-webapp-$version.war"
context.contextPath = contextPath; context.contextPath = contextPath
} }

View File

@@ -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

View File

@@ -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-----

View File

@@ -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-----