From eaf6fc6e2ddf73b3476151d62553c5c881114b9f Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 24 Feb 2024 18:36:11 +0100 Subject: [PATCH] Add pairgoth-common module to share sources between api and view webapps --- api-webapp/pom.xml | 5 + .../org/jeudego/pairgoth/server/ApiServlet.kt | 2 +- .../jeudego/pairgoth/server/WebappManager.kt | 165 +--------- .../org/jeudego/pairgoth/store/Store.kt | 4 +- pairgoth-common/pom.xml | 150 +++++++++ pairgoth-common/pom.xml.versionsBackup | 307 ++++++++++++++++++ .../jeudego/pairgoth/util/AESCryptograph.kt | 0 .../org/jeudego/pairgoth/util/Colorizer.kt | 0 .../org/jeudego/pairgoth/util/Cryptograph.kt | 0 .../org/jeudego/pairgoth/util/JsonIO.kt | 0 .../org/jeudego/pairgoth/util/XmlUtils.kt | 0 .../jeudego/pairgoth/web/BaseWebappManager.kt | 147 +++++++++ .../org/jeudego/pairgoth/web/Logging.kt | 73 +++++ pom.xml | 1 + view-webapp/pom.xml | 5 + .../pairgoth/ratings/RatingsHandler.kt | 2 +- .../pairgoth/ratings/RatingsManager.kt | 2 +- .../org/jeudego/pairgoth/util/Colorizer.kt | 18 - .../org/jeudego/pairgoth/util/JsonIO.kt | 25 -- .../org/jeudego/pairgoth/view/ApiTool.kt | 2 +- .../org/jeudego/pairgoth/web/ApiServlet.kt | 2 +- .../org/jeudego/pairgoth/web/AuthFilter.kt | 3 +- .../org/jeudego/pairgoth/web/LoginServlet.kt | 4 +- .../org/jeudego/pairgoth/web/WebappManager.kt | 178 ++-------- 24 files changed, 733 insertions(+), 362 deletions(-) create mode 100644 pairgoth-common/pom.xml create mode 100644 pairgoth-common/pom.xml.versionsBackup rename {view-webapp => pairgoth-common}/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt (100%) rename {api-webapp => pairgoth-common}/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt (100%) rename {view-webapp => pairgoth-common}/src/main/kotlin/org/jeudego/pairgoth/util/Cryptograph.kt (100%) rename {api-webapp => pairgoth-common}/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt (100%) rename {api-webapp => pairgoth-common}/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt (100%) create mode 100644 pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/BaseWebappManager.kt create mode 100644 pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Logging.kt delete mode 100644 view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt delete mode 100644 view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt diff --git a/api-webapp/pom.xml b/api-webapp/pom.xml index aefa2a2..4ddf8bc 100644 --- a/api-webapp/pom.xml +++ b/api-webapp/pom.xml @@ -114,6 +114,11 @@ + + org.jeudego.pairgoth + pairgoth-common + ${project.version} + org.jetbrains.kotlin diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt index 343dfc3..f37dbb1 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt @@ -65,7 +65,7 @@ class ApiServlet: HttpServlet() { // validate request - if ("dev" == WebappManager.getProperty("env")) { + if ("dev" == WebappManager.properties["env"]) { response.addHeader("Access-Control-Allow-Origin", "*") } validateAccept(request); diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/WebappManager.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/WebappManager.kt index bbddeed..dc7412b 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/WebappManager.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/WebappManager.kt @@ -1,171 +1,30 @@ package org.jeudego.pairgoth.server import com.republicate.mailer.SmtpLoop -import org.apache.commons.lang3.tuple.Pair -import org.slf4j.LoggerFactory -import java.io.IOException -import java.lang.IllegalAccessError -import java.security.SecureRandom -import java.security.cert.X509Certificate -import java.util.* -import java.util.IllegalFormatCodePointException -import javax.net.ssl.* +import org.jeudego.pairgoth.web.BaseWebappManager import javax.servlet.* import javax.servlet.annotation.WebListener -import javax.servlet.http.HttpSessionEvent -import javax.servlet.http.HttpSessionListener @WebListener -class WebappManager : ServletContextListener, ServletContextAttributeListener, HttpSessionListener { - private fun disableSSLCertificateChecks() { - // see http://www.nakov.com/blog/2009/07/16/disable-certificate-validation-in-java-ssl-connections/ - try { - // Create a trust manager that does not validate certificate chains - val trustAllCerts = arrayOf(object : X509TrustManager { - override fun getAcceptedIssuers(): Array? { - return null - } - - @Suppress("TrustAllX509TrustManager") - override fun checkClientTrusted(certs: Array, authType: String) {} - @Suppress("TrustAllX509TrustManager") - override fun checkServerTrusted(certs: Array, authType: String) {} - } - ) - - // Install the all-trusting trust manager - val sc = SSLContext.getInstance("SSL") - sc.init(null, trustAllCerts, SecureRandom()) - HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory) - - // Create all-trusting host name verifier - val allHostsValid = HostnameVerifier { hostname, session -> true } - - // Install the all-trusting host verifier - HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) - } catch (e: Exception) { - logger.error("could not disable SSL certificate checks", e) - } - } +class WebappManager : BaseWebappManager("API Server","api") { /* ServletContextListener interface */ override fun contextInitialized(sce: ServletContextEvent) { - context = sce.servletContext - logger.info("---------- Starting $WEBAPP_NAME ----------") - webappRoot = context.getRealPath("/") - try { - // load default properties - properties.load(context.getResourceAsStream("/WEB-INF/pairgoth.default.properties")) - // override with system properties after stripping off the 'pairgoth.' prefix - System.getProperties().filter { (key, value) -> key is String && key.startsWith(PAIRGOTH_PROPERTIES_PREFIX) - }.forEach { (key, value) -> - properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value - } + super.contextInitialized(sce) - logger.info("pairgoth server ${properties["version"]} with profile ${properties["env"]}") + logger.info("pairgoth server ${properties["version"]} with profile ${properties["env"]}") - // set system user agent string to empty string - System.setProperty("http.agent", "") - - // disable (for now ?) the SSL certificate checks, because many sites - // fail to correctly implement SSL... - disableSSLCertificateChecks() - - // start smtp loop - if (properties.containsKey("smtp.host")) { - registerService("smtp", SmtpLoop(properties)) - startService("smtp") - } - - } catch (ioe: IOException) { - logger.error("webapp initialization error", ioe) + // start smtp loop + if (properties.containsKey("smtp.host")) { + logger.info("Launching SMTP loop") + registerService("smtp", SmtpLoop(properties)) + startService("smtp") } } - override fun contextDestroyed(sce: ServletContextEvent) { - logger.info("---------- Stopping $WEBAPP_NAME ----------") - - stopService("smtp"); - - val context = sce.servletContext - for (service in webServices.keys) stopService(service, true) - // ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...); - - logger.info("---------- Stopped $WEBAPP_NAME ----------") - } - - /* ServletContextAttributeListener interface */ - override fun attributeAdded(event: ServletContextAttributeEvent) {} - override fun attributeRemoved(event: ServletContextAttributeEvent) {} - override fun attributeReplaced(event: ServletContextAttributeEvent) {} - - /* HttpSessionListener interface */ - override fun sessionCreated(se: HttpSessionEvent) {} - override fun sessionDestroyed(se: HttpSessionEvent) {} - companion object { - const val WEBAPP_NAME = "Pairgoth API Server" - const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth." - lateinit var webappRoot: String - lateinit var context: ServletContext - private val webServices: MutableMap> = TreeMap() - var logger = LoggerFactory.getLogger(WebappManager::class.java) - val properties = Properties() - fun getProperty(prop: String): String? { - return properties.getProperty(prop) - } - fun getMandatoryProperty(prop: String): String { - return getProperty(prop) ?: throw Error("missing property: ${prop}") - } - - val webappURL by lazy { getProperty("webapp.external.url") } - - private val services = mutableMapOf>() - - @JvmOverloads - fun registerService(name: String?, task: Runnable, initialStatus: Boolean? = null) { - if (webServices.containsKey(name)) { - logger.warn("service {} already registered") - return - } - logger.debug("registered service {}", name) - webServices[name] = - Pair.of(task, null) - } - - fun startService(name: String?) { - val service = webServices[name]!! - if (service.right != null && service.right!!.isAlive) { - logger.warn("service {} is already running", name) - return - } - logger.debug("starting service {}", name) - val thread = Thread(service.left, name) - thread.start() - webServices[name] = - Pair.of( - service.left, - thread - ) - } - - @JvmOverloads - fun stopService(name: String?, webappClosing: Boolean = false) { - val service = webServices[name]!! - val thread = service.right - if (thread == null || !thread.isAlive) { - logger.warn("service {} is already stopped", name) - return - } - logger.debug("stopping service {}", name) - thread.interrupt() - try { - thread.join() - } catch (ie: InterruptedException) { - } - if (!webappClosing) { - webServices[name] = Pair.of(service.left, null) - } - } + val properties get() = BaseWebappManager.properties + fun getMandatoryProperty(prop: String) = properties.getProperty(prop) ?: throw Error("missing property: $prop") + val context get() = BaseWebappManager.context } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt index fd95cca..34f8fc5 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/Store.kt @@ -3,10 +3,10 @@ package org.jeudego.pairgoth.store import org.jeudego.pairgoth.server.WebappManager private fun createStoreImplementation(): IStore { - return when (val storeProperty = WebappManager.getProperty("store") ?: "memory") { + return when (val storeProperty = WebappManager.properties.getProperty("store") ?: "memory") { "memory" -> MemoryStore() "file" -> { - val filePath = WebappManager.getProperty("store.file.path") ?: "." + val filePath = WebappManager.properties.getProperty("store.file.path") ?: "." FileStore(filePath) } else -> throw Error("unknown store: $storeProperty") diff --git a/pairgoth-common/pom.xml b/pairgoth-common/pom.xml new file mode 100644 index 0000000..4b545b7 --- /dev/null +++ b/pairgoth-common/pom.xml @@ -0,0 +1,150 @@ + + + 4.0.0 + + + org.jeudego.pairgoth + engine-parent + 0.3 + + pairgoth-common + + jar + ${project.groupId}:${project.artifactId} + Pairgoth common utilities + TODO + + package + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + + + + + org.junit + junit-bom + 5.9.3 + pom + import + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlinx + kotlinx-datetime-jvm + 0.4.0 + + + + jakarta.servlet + jakarta.servlet-api + ${servlet.api.version} + provided + + + com.sun.mail + jakarta.mail + 1.6.7 + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + commons-io + commons-io + 2.13.0 + + + + io.github.microutils + kotlin-logging-jvm + 3.0.5 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + com.diogonunes + JColor + 5.0.1 + + + + com.republicate + simple-mailer + 1.6 + + + + com.republicate.kson + essential-kson-jvm + 2.4 + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.mockito.kotlin + mockito-kotlin + 4.1.0 + test + + + diff --git a/pairgoth-common/pom.xml.versionsBackup b/pairgoth-common/pom.xml.versionsBackup new file mode 100644 index 0000000..3c2381a --- /dev/null +++ b/pairgoth-common/pom.xml.versionsBackup @@ -0,0 +1,307 @@ + + + 4.0.0 + + + org.jeudego.pairgoth + engine-parent + 1.0-SNAPSHOT + + api-webapp + + war + ${project.groupId}:${project.artifactId} + PairGoth pairing system + TODO + + 5.7.1 + + + package + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + com.republicate:webapp-slf4j-logger + + + ${project.build.testOutputDirectory} + + + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + 0 + + ${pairgoth.api.host} + ${pairgoth.api.port} + + + ${pairgoth.env} + ${pairgoth.version} + ${pairgoth.api.external.url} + ${pairgoth.webapp.external.url} + ${pairgoth.store} + ${pairgoth.store.file.path} + ${pairgoth.logger.level} + ${pairgoth.logger.format} + + + ${pairgoth.api.context}/ + + + + + org.codehaus.mojo + jaxb2-maven-plugin + 3.1.0 + + + gen-schema + + xjc + + + + + org.jeudego.pairgoth.opengotha + XmlSchema + + src/main/resources/xsd + + + + + + + + + org.junit + junit-bom + 5.9.3 + pom + import + + + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlinx + kotlinx-datetime-jvm + 0.4.0 + + + + jakarta.servlet + jakarta.servlet-api + ${servlet.api.version} + provided + + + com.sun.mail + jakarta.mail + 1.6.7 + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + commons-io + commons-io + 2.13.0 + + + + org.pac4j + pac4j-oauth + ${pac4j.version} + + + + io.github.microutils + kotlin-logging-jvm + 3.0.5 + + + org.slf4j + slf4j-api + ${slf4j.version} + + + com.republicate + webapp-slf4j-logger + 3.0 + runtime + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + com.diogonunes + JColor + 5.0.1 + + + + com.republicate + simple-mailer + 1.6 + + + + com.republicate.kson + essential-kson-jvm + 2.4 + + + + + + org.apache.pdfbox + pdfbox + 2.0.28 + + + + com.republicate + jeasse-servlet3 + 1.2 + + + + org.jgrapht + jgrapht-core + 1.5.2 + + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.0 + + + org.glassfish.jaxb + jaxb-runtime + 4.0.3 + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.mockito.kotlin + mockito-kotlin + 4.1.0 + test + + + + com.icegreen + greenmail + 1.6.12 + test + + + junit + junit + + + javax.activation + activation + + + com.sun.mail + javax.mail + + + + + diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt similarity index 100% rename from view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt rename to pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/AESCryptograph.kt diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt similarity index 100% rename from api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt rename to pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Cryptograph.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Cryptograph.kt similarity index 100% rename from view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Cryptograph.kt rename to pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/Cryptograph.kt diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt similarity index 100% rename from api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt rename to pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt similarity index 100% rename from api-webapp/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt rename to pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/XmlUtils.kt diff --git a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/BaseWebappManager.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/BaseWebappManager.kt new file mode 100644 index 0000000..5e2c143 --- /dev/null +++ b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/BaseWebappManager.kt @@ -0,0 +1,147 @@ +package org.jeudego.pairgoth.web + +import com.republicate.mailer.SmtpLoop +import org.apache.commons.lang3.tuple.Pair +import org.slf4j.LoggerFactory +import java.io.IOException +import java.lang.IllegalAccessError +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.util.* +import java.util.IllegalFormatCodePointException +import javax.net.ssl.* +import javax.servlet.* +import javax.servlet.annotation.WebListener +import javax.servlet.http.HttpSessionEvent +import javax.servlet.http.HttpSessionListener + +abstract class BaseWebappManager(val webappName: String, loggerName: String) : ServletContextListener, ServletContextAttributeListener, HttpSessionListener { + + val logger = LoggerFactory.getLogger(loggerName) + + protected fun disableSSLCertificateChecks() { + // see http://www.nakov.com/blog/2009/07/16/disable-certificate-validation-in-java-ssl-connections/ + try { + // Create a trust manager that does not validate certificate chains + val trustAllCerts = arrayOf(object : X509TrustManager { + override fun getAcceptedIssuers(): Array? { + return null + } + + @Suppress("TrustAllX509TrustManager") + override fun checkClientTrusted(certs: Array, authType: String) {} + @Suppress("TrustAllX509TrustManager") + override fun checkServerTrusted(certs: Array, authType: String) {} + } + ) + + // Install the all-trusting trust manager + val sc = SSLContext.getInstance("SSL") + sc.init(null, trustAllCerts, SecureRandom()) + HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory) + + // Create all-trusting host name verifier + val allHostsValid = HostnameVerifier { hostname, session -> true } + + // Install the all-trusting host verifier + HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) + } catch (e: Exception) { + logger.error("could not disable SSL certificate checks", e) + } + } + + private val webServices: MutableMap> = TreeMap() + + @JvmOverloads + fun registerService(name: String?, task: Runnable, initialStatus: Boolean? = null) { + if (webServices.containsKey(name)) { + logger.warn("service {} already registered") + return + } + logger.debug("registered service {}", name) + webServices[name] = + Pair.of(task, null) + } + + fun startService(name: String?) { + val service = webServices[name]!! + if (service.right != null && service.right!!.isAlive) { + logger.warn("service {} is already running", name) + return + } + logger.debug("starting service {}", name) + val thread = Thread(service.left, name) + thread.start() + webServices[name] = + Pair.of( + service.left, + thread + ) + } + + @JvmOverloads + fun stopService(name: String?, webappClosing: Boolean = false) { + val service = webServices[name]!! + val thread = service.right + if (thread == null || !thread.isAlive) { + logger.warn("service {} is already stopped", name) + return + } + logger.debug("stopping service {}", name) + thread.interrupt() + try { + thread.join() + } catch (ie: InterruptedException) { + } + if (!webappClosing) { + webServices[name] = Pair.of(service.left, null) + } + } + + /* ServletContextListener interface */ + override fun contextInitialized(sce: ServletContextEvent) { + context = sce.servletContext + logger.info("---------- Starting $webappName ----------") + webappRoot = context.getRealPath("/") + try { + // load default properties + properties.load(context.getResourceAsStream("/WEB-INF/pairgoth.default.properties")) + // override with system properties after stripping off the 'pairgoth.' prefix + System.getProperties().filter { (key, value) -> key is String && key.startsWith(PAIRGOTH_PROPERTIES_PREFIX) + }.forEach { (key, value) -> + properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value + } + + // set system user agent string to empty string + System.setProperty("http.agent", "") + + // disable (for now ?) the SSL certificate checks, because many sites + // fail to correctly implement SSL... + disableSSLCertificateChecks() + + } catch (ioe: IOException) { + logger.error("webapp initialization error", ioe) + } + } + + override fun contextDestroyed(sce: ServletContextEvent) { + logger.info("---------- Stopping $webappName ----------") + for (service in webServices.keys) stopService(service, true) + } + + /* ServletContextAttributeListener interface */ + override fun attributeAdded(event: ServletContextAttributeEvent) {} + override fun attributeRemoved(event: ServletContextAttributeEvent) {} + override fun attributeReplaced(event: ServletContextAttributeEvent) {} + + /* HttpSessionListener interface */ + override fun sessionCreated(se: HttpSessionEvent) {} + override fun sessionDestroyed(se: HttpSessionEvent) {} + + companion object { + const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth." + lateinit var webappRoot: String + lateinit var context: ServletContext + val properties = Properties() + } +} diff --git a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Logging.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Logging.kt new file mode 100644 index 0000000..ae4f8e3 --- /dev/null +++ b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/web/Logging.kt @@ -0,0 +1,73 @@ +package org.jeudego.pairgoth.web + +import com.republicate.kson.Json +import org.jeudego.pairgoth.util.Colorizer.blue +import org.jeudego.pairgoth.util.Colorizer.green +import org.jeudego.pairgoth.util.toString +import org.slf4j.Logger +import java.io.StringWriter +import javax.servlet.http.HttpServletRequest + +fun Logger.logRequest(req: HttpServletRequest, logHeaders: Boolean = false) { + val builder = StringBuilder() + builder.append(req.method).append(' ') + .append(req.scheme).append("://") + .append(req.localName) + val port = req.localPort + if (port != 80) builder.append(':').append(port) + /* + if (!req.contextPath.isEmpty()) { + builder.append(req.contextPath) + } + */ + builder.append(req.requestURI) + if (req.method == "GET") { + val qs = req.queryString + if (qs != null) builder.append('?').append(qs) + } + // builder.append(' ').append(req.getProtocol()); + info(blue("<< {}"), builder.toString()) + if (isTraceEnabled && logHeaders) { + // CB TODO - should be bufferized and asynchronously written in synchronous chunks + // so that header lines from parallel requests are not mixed up in the logs ; + // synchronizing the whole request log is not desirable + val headerNames = req.headerNames + while (headerNames.hasMoreElements()) { + val name = headerNames.nextElement() + val value = req.getHeader(name) + trace(blue("<< {}: {}"), name, value) + } + } +} + +fun Logger.logPayload(prefix: String?, payload: Json, upstream: Boolean) { + val writer = StringWriter() + //payload.toPrettyString(writer, ""); + payload.toString(writer) + if (isTraceEnabled) { + for (line in writer.toString().split("\n".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()) { + trace(if (upstream) blue("{}{}") else green("{}{}"), prefix, line) + } + } else { + var line = writer.toString() + val pos = line.indexOf('\n') + if (pos != -1) line = line.substring(0, pos) + if (line.length > 50) line = line.substring(0, 50) + "..." + debug(if (upstream) blue("{}{}") else green("{}{}"), prefix, line) + } +} + + +fun HttpServletRequest.getRemoteAddress(): String? { + var ip = getHeader("X-Forwarded-For") + if (ip == null) { + ip = remoteAddr + } else { + val comma = ip.indexOf(',') + if (comma != -1) { + ip = ip.substring(0, comma).trim { it <= ' ' } // keep the left-most IP address + } + } + return ip +} diff --git a/pom.xml b/pom.xml index 307653f..15348a1 100644 --- a/pom.xml +++ b/pom.xml @@ -93,6 +93,7 @@ + pairgoth-common api-webapp view-webapp webserver diff --git a/view-webapp/pom.xml b/view-webapp/pom.xml index 666107d..a96c5a8 100644 --- a/view-webapp/pom.xml +++ b/view-webapp/pom.xml @@ -137,6 +137,11 @@ + + org.jeudego.pairgoth + pairgoth-common + ${project.version} + org.jetbrains.kotlin diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt index 15eeb51..262b06d 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsHandler.kt @@ -22,7 +22,7 @@ abstract class RatingsHandler(val origin: RatingsManager.Ratings) { private var updated = false val url: URL by lazy { - WebappManager.getProperty("ratings.${origin.name.lowercase(Locale.ROOT)}")?.let { URL(it) } ?: defaultURL + WebappManager.properties.getProperty("ratings.${origin.name.lowercase(Locale.ROOT)}")?.let { URL(it) } ?: defaultURL } fun updateIfNeeded(): Boolean { diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt index 3e20eab..e79d268 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/ratings/RatingsManager.kt @@ -91,7 +91,7 @@ object RatingsManager: Runnable { } } val logger = LoggerFactory.getLogger("ratings") - val path = Path.of(WebappManager.getProperty("ratings.path") ?: "ratings").also { + val path = Path.of(WebappManager.properties.getProperty("ratings.path") ?: "ratings").also { val file = it.toFile() if (!file.mkdirs() && !file.isDirectory) throw Error("Property pairgoth.ratings.path must be a directory") } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt deleted file mode 100644 index f3d2a09..0000000 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/Colorizer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.jeudego.pairgoth.util - -import com.diogonunes.jcolor.Ansi -import com.diogonunes.jcolor.AnsiFormat -import com.diogonunes.jcolor.Attribute - -private val blue = AnsiFormat(Attribute.BRIGHT_BLUE_TEXT()) -private val green = AnsiFormat(Attribute.BRIGHT_GREEN_TEXT()) -private val red = AnsiFormat(Attribute.BRIGHT_RED_TEXT()) -private val bold = AnsiFormat(Attribute.BOLD()) - -object Colorizer { - - fun blue(str: String) = Ansi.colorize(str, blue) - fun green(str: String) = Ansi.colorize(str, green) - fun red(str: String) = Ansi.colorize(str, red) - fun bold(str: String) = Ansi.colorize(str, bold) -} diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt deleted file mode 100644 index 99177ea..0000000 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/util/JsonIO.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.jeudego.pairgoth.util - -import com.republicate.kson.Json -import java.io.Reader -import java.io.Writer - - -fun Json.Companion.parse(reader: Reader) = Json.Companion.parse(object: Json.Input { - override fun read() = reader.read().toChar() -}) - -fun Json.toString(writer: Writer) = toString(object: Json.Output { - override fun writeChar(c: Char): Json.Output { - writer.write(c.code) - return this - } - override fun writeString(s: String): Json.Output { - writer.write(s) - return this - } - override fun writeString(s: String, from: Int, to: Int): Json.Output { - writer.write(s, from, to) - return this - } -}) diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt index 913e483..5a66066 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/ApiTool.kt @@ -14,7 +14,7 @@ class ApiTool { const val JSON = "application/json" const val XML = "application/xml" val apiRoot by lazy { - WebappManager.getProperty("api.external.url")?.let { "${it.removeSuffix("/")}/" } + WebappManager.properties.getProperty("api.external.url")?.let { "${it.removeSuffix("/")}/" } ?: throw Error("no configured API url") } val logger = LoggerFactory.getLogger("api") diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt index b4159b6..eeb5c61 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/ApiServlet.kt @@ -20,7 +20,7 @@ class ApiServlet : AsyncProxyServlet() { companion object { private val apiRoot by lazy { - WebappManager.getProperty("api.external.url")?.let { "${it.removeSuffix("/")}" } + WebappManager.properties.getProperty("api.external.url")?.let { "${it.removeSuffix("/")}" } ?: throw Error("no configured API url") } } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt index 18078ca..f08922b 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/AuthFilter.kt @@ -28,7 +28,7 @@ class AuthFilter: Filter { val response = resp as HttpServletResponse val uri = request.requestURI val session: HttpSession? = request.getSession(false) - val auth = WebappManager.getProperty("auth") ?: throw Error("authentication not configured") + val auth = WebappManager.properties.getProperty("auth") ?: throw Error("authentication not configured") val forwarded = request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) != null if (auth == "oauth" && uri.startsWith("/oauth/")) { @@ -66,6 +66,5 @@ class AuthFilter: Filter { val nolangUri = uri.replace(Regex("^/../"), "/") return whitelist.contains(nolangUri) } - } } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt index b57a4c1..f301b90 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/LoginServlet.kt @@ -16,7 +16,7 @@ class LoginServlet: HttpServlet() { val mimeType = if (sep == -1) contentType else contentType.substring(0, sep).trim { it <= ' ' } if (!isJson(mimeType)) throw Error("expecting json") val payload = Json.Companion.parse(req.reader.readText())?.asObject() ?: throw Error("null json") - val user = when (WebappManager.getProperty("auth")) { + val user = when (WebappManager.properties.getProperty("auth")) { "sesame" -> checkSesame(payload) else -> checkLoginPass(payload) } ?: throw Error("authentication failed") @@ -33,7 +33,7 @@ class LoginServlet: HttpServlet() { } fun checkSesame(payload: Json.Object): Json.Object? { - val expected = WebappManager.getProperty("auth.sesame") ?: throw Error("sesame wrongly configured") + val expected = WebappManager.properties.getProperty("auth.sesame") ?: throw Error("sesame wrongly configured") return if (payload.getString("sesame")?.equals(expected) == true) Json.Object("logged" to true) else null } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt index f466c68..d8cdbee 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/web/WebappManager.kt @@ -16,175 +16,43 @@ import javax.servlet.http.HttpSessionEvent import javax.servlet.http.HttpSessionListener @WebListener -class WebappManager : ServletContextListener, ServletContextAttributeListener, HttpSessionListener { - private fun disableSSLCertificateChecks() { - // see http://www.nakov.com/blog/2009/07/16/disable-certificate-validation-in-java-ssl-connections/ - try { - // Create a trust manager that does not validate certificate chains - val trustAllCerts = arrayOf(object : X509TrustManager { - override fun getAcceptedIssuers(): Array? { - return null - } - - @Suppress("TrustAllX509TrustManager") - override fun checkClientTrusted(certs: Array, authType: String) {} - @Suppress("TrustAllX509TrustManager") - override fun checkServerTrusted(certs: Array, authType: String) {} - } - ) - - // Install the all-trusting trust manager - val sc = SSLContext.getInstance("SSL") - sc.init(null, trustAllCerts, SecureRandom()) - HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory) - - // Create all-trusting host name verifier - val allHostsValid = HostnameVerifier { hostname, session -> true } - - // Install the all-trusting host verifier - HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid) - } catch (e: Exception) { - logger.error("could not disable SSL certificate checks", e) - } - } +class WebappManager : BaseWebappManager("View Webapp", "view") { /* ServletContextListener interface */ override fun contextInitialized(sce: ServletContextEvent) { - context = sce.servletContext - logger.info("---------- Starting $WEBAPP_NAME ----------") - logger.info("info level is active") - logger.debug("debug level is active") - logger.trace("trace level is active") - webappRoot = context.getRealPath("/") - try { - // load default properties - properties.load(context.getResourceAsStream("/WEB-INF/pairgoth.default.properties")) - // override with system properties after stripping off the 'pairgoth.' prefix - System.getProperties().filter { (key, value) -> key is String && key.startsWith(PAIRGOTH_PROPERTIES_PREFIX) - }.forEach { (key, value) -> - properties[(key as String).removePrefix(PAIRGOTH_PROPERTIES_PREFIX)] = value - } + super.contextInitialized(sce) - logger.info("pairgoth server ${properties["version"]} with profile ${properties["env"]}") - - // publish some properties to the webapp context; for easy access from the template - context.setAttribute("env", properties.getProperty("env") ?: "dev") - context.setAttribute("version", properties.getProperty("version") ?: "?") - val auth = properties.getProperty("auth") ?: "none" - context.setAttribute("auth", auth) - when (auth) { - "none", "sesame" -> {} - "oauth" -> { - properties.getProperty("oauth.providers")?.let { - val providers = it.split(Regex("\\s*,\\s*")) - context.setAttribute("oauthProviders", providers) - providers.forEach { provider -> - context.setAttribute("${provider}Provider", OauthHelperFactory.getHelper(provider)) - } + // publish some properties to the webapp context; for easy access from the template + context.setAttribute("env", properties.getProperty("env") ?: "dev") + context.setAttribute("version", properties.getProperty("version") ?: "?") + val auth = properties.getProperty("auth") ?: "none" + context.setAttribute("auth", auth) + when (auth) { + "none", "sesame" -> {} + "oauth" -> { + properties.getProperty("oauth.providers")?.let { + val providers = it.split(Regex("\\s*,\\s*")) + context.setAttribute("oauthProviders", providers) + providers.forEach { provider -> + context.setAttribute("${provider}Provider", OauthHelperFactory.getHelper(provider)) } } - else -> throw Error("Unhandled auth: $auth") } - - // set system user agent string to empty string - System.setProperty("http.agent", "") - - // disable (for now ?) the SSL certificate checks, because many sites - // fail to correctly implement SSL... - disableSSLCertificateChecks() - - registerService("ratings", RatingsManager) - startService("ratings") - - } catch (ioe: IOException) { - logger.error("webapp initialization error", ioe) + else -> throw Error("Unhandled auth: $auth") } + + registerService("ratings", RatingsManager) + startService("ratings") } override fun contextDestroyed(sce: ServletContextEvent) { - logger.info("---------- Stopping $WEBAPP_NAME ----------") - + super.contextDestroyed(sce) Translator.notifyExiting() - - val context = sce.servletContext - for (service in webServices.keys) stopService(service, true) - // ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...); - - logger.info("---------- Stopped $WEBAPP_NAME ----------") } - /* ServletContextAttributeListener interface */ - override fun attributeAdded(event: ServletContextAttributeEvent) {} - override fun attributeRemoved(event: ServletContextAttributeEvent) {} - override fun attributeReplaced(event: ServletContextAttributeEvent) {} - - /* HttpSessionListener interface */ - override fun sessionCreated(se: HttpSessionEvent) {} - override fun sessionDestroyed(se: HttpSessionEvent) {} - companion object { - const val WEBAPP_NAME = "Pairgoth Web Client" - const val PAIRGOTH_PROPERTIES_PREFIX = "pairgoth." - lateinit var webappRoot: String - lateinit var context: ServletContext - private val webServices: MutableMap> = TreeMap() - var logger = LoggerFactory.getLogger(WebappManager::class.java) - val properties = Properties() - fun getProperty(prop: String): String? { - return properties.getProperty(prop) - } - fun getMandatoryProperty(prop: String): String { - return getProperty(prop) ?: throw Error("missing property: ${prop}") - } - - val webappURL by lazy { getProperty("webapp.external.url") } - - private val services = mutableMapOf>() - - @JvmOverloads - fun registerService(name: String?, task: Runnable, initialStatus: Boolean? = null) { - if (webServices.containsKey(name)) { - logger.warn("service {} already registered") - return - } - logger.debug("registered service {}", name) - webServices[name] = - Pair.of(task, null) - } - - fun startService(name: String?) { - val service = webServices[name]!! - if (service.right != null && service.right!!.isAlive) { - logger.warn("service {} is already running", name) - return - } - logger.debug("starting service {}", name) - val thread = Thread(service.left, name) - thread.start() - webServices[name] = - Pair.of( - service.left, - thread - ) - } - - @JvmOverloads - fun stopService(name: String?, webappClosing: Boolean = false) { - val service = webServices[name]!! - val thread = service.right - if (thread == null || !thread.isAlive) { - logger.warn("service {} is already stopped", name) - return - } - logger.debug("stopping service {}", name) - thread.interrupt() - try { - thread.join() - } catch (ie: InterruptedException) { - } - if (!webappClosing) { - webServices[name] = Pair.of(service.left, null) - } - } + val properties get() = BaseWebappManager.properties + val context get() = BaseWebappManager.context + fun getMandatoryProperty(prop: String) = properties.getProperty(prop) ?: throw Error("missing property: $prop") } }