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