View webapp in progress
This commit is contained in:
262
view-webapp/pom.xml
Normal file
262
view-webapp/pom.xml
Normal file
@@ -0,0 +1,262 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.jeudego.pairgoth</groupId>
|
||||
<artifactId>engine-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>view-webapp</artifactId>
|
||||
|
||||
<packaging>war</packaging>
|
||||
<name>${project.groupId}:${project.artifactId}</name>
|
||||
<description>PairGoth pairing system</description>
|
||||
<url>TODO</url>
|
||||
<properties>
|
||||
<kotlin.version>1.8.21</kotlin.version>
|
||||
<kotlin.code.style>official</kotlin.code.style>
|
||||
<kotlin.compiler.jvmTarget>10</kotlin.compiler.jvmTarget>
|
||||
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
|
||||
<pac4j.version>5.7.1</pac4j.version>
|
||||
</properties>
|
||||
<build>
|
||||
<defaultGoal>package</defaultGoal>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
<phase>process-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.basedir}/src/main/config</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<classpathDependencyExcludes>
|
||||
<classpathDependencyExclude>com.republicate:webapp-slf4j-logger</classpathDependencyExclude>
|
||||
</classpathDependencyExcludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>5.9.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- main dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test-junit5</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-datetime-jvm</artifactId>
|
||||
<version>0.4.0</version>
|
||||
</dependency>
|
||||
<!-- servlets and mail APIs -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>${servlet.api.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>1.6.7</version>
|
||||
</dependency>
|
||||
<!-- auth -->
|
||||
<dependency>
|
||||
<groupId>org.pac4j</groupId>
|
||||
<artifactId>pac4j-oauth</artifactId>
|
||||
<version>${pac4j.version}</version>
|
||||
</dependency>
|
||||
<!-- logging -->
|
||||
<dependency>
|
||||
<groupId>io.github.microutils</groupId>
|
||||
<artifactId>kotlin-logging-jvm</artifactId>
|
||||
<version>3.0.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.republicate</groupId>
|
||||
<artifactId>webapp-slf4j-logger</artifactId>
|
||||
<version>3.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.diogonunes</groupId>
|
||||
<artifactId>JColor</artifactId>
|
||||
<version>5.0.1</version>
|
||||
</dependency>
|
||||
<!-- mailer -->
|
||||
<dependency>
|
||||
<groupId>com.republicate</groupId>
|
||||
<artifactId>simple-mailer</artifactId>
|
||||
<version>1.6</version>
|
||||
</dependency>
|
||||
<!-- json -->
|
||||
<dependency>
|
||||
<groupId>com.republicate.kson</groupId>
|
||||
<artifactId>essential-kson-jvm</artifactId>
|
||||
<version>2.3</version>
|
||||
</dependency>
|
||||
<!-- charset detection
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>70.1</version>
|
||||
</dependency>
|
||||
-->
|
||||
<!-- net clients
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.13</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.8.0</version>
|
||||
</dependency>
|
||||
-->
|
||||
<!-- server-side events -->
|
||||
<dependency>
|
||||
<groupId>com.republicate</groupId>
|
||||
<artifactId>jeasse-servlet3</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<!-- templating -->
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity.tools</groupId>
|
||||
<artifactId>velocity-tools-view</artifactId>
|
||||
<version>3.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.velocity</groupId>
|
||||
<artifactId>velocity-engine-core</artifactId>
|
||||
<version>2.4-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<!-- tests -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>${junit.jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito.kotlin</groupId>
|
||||
<artifactId>mockito-kotlin</artifactId>
|
||||
<version>4.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- test emails -->
|
||||
<dependency>
|
||||
<groupId>com.icegreen</groupId>
|
||||
<artifactId>greenmail</artifactId>
|
||||
<version>1.6.12</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>javax.activation</groupId>
|
||||
<artifactId>activation</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>javax.mail</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
10
view-webapp/src/main/config/jetty-web.xml
Normal file
10
view-webapp/src/main/config/jetty-web.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
|
||||
<Configure class="org.eclipse.jetty.webapp.WebAppContext">
|
||||
<!-- https://www.eclipse.org/jetty/documentation/jetty-9/index.html#file-alias-serving -->
|
||||
<Call name="addAliasCheck">
|
||||
<Arg>
|
||||
<New class="org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker" />
|
||||
</Arg>
|
||||
</Call>
|
||||
</Configure>
|
18
view-webapp/src/main/config/pairgoth.default.properties
Normal file
18
view-webapp/src/main/config/pairgoth.default.properties
Normal file
@@ -0,0 +1,18 @@
|
||||
# webapp
|
||||
webapp.env = dev
|
||||
webapp.url = http://localhost:8080
|
||||
|
||||
# store
|
||||
store = file
|
||||
store.file.path = tournamentfiles
|
||||
|
||||
# smtp
|
||||
smtp.sender =
|
||||
smtp.host =
|
||||
smtp.port = 587
|
||||
smtp.user =
|
||||
smtp.password =
|
||||
|
||||
# logging
|
||||
logger.level = trace
|
||||
logger.format = [%level] %ip [%logger] %message
|
0
view-webapp/src/main/config/translations/fr
Normal file
0
view-webapp/src/main/config/translations/fr
Normal file
56
view-webapp/src/main/config/web.xml
Normal file
56
view-webapp/src/main/config/web.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
|
||||
version="3.1">
|
||||
<!-- Listeners -->
|
||||
<!-- we're not using @WebListener annotations so that our manager is initialized *after* the webapp logger -->
|
||||
<listener>
|
||||
<listener-class>com.republicate.slf4j.impl.ServletContextLoggerListener</listener-class>
|
||||
</listener>
|
||||
<listener>
|
||||
<listener-class>org.jeudego.pairgoth.web.WebappManager</listener-class>
|
||||
</listener>
|
||||
|
||||
<!-- filters -->
|
||||
<filter>
|
||||
<filter-name>webapp-slf4j-logger-ip-tag-filter</filter-name>
|
||||
<filter-class>com.republicate.slf4j.impl.IPTagFilter</filter-class>
|
||||
<async-supported>true</async-supported>
|
||||
</filter>
|
||||
|
||||
<!-- filters mapping -->
|
||||
<filter-mapping>
|
||||
<filter-name>webapp-slf4j-logger-ip-tag-filter</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
<dispatcher>REQUEST</dispatcher>
|
||||
<dispatcher>FORWARD</dispatcher>
|
||||
</filter-mapping>
|
||||
|
||||
<!-- servlets -->
|
||||
<servlet>
|
||||
<servlet-name>view</servlet-name>
|
||||
<servlet-class>org.jeudego.pairgoth.web.ViewServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet>
|
||||
<servlet-name>sse</servlet-name>
|
||||
<servlet-class>org.jeudego.pairgoth.web.SSEServlet</servlet-class>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
<async-supported>true</async-supported>
|
||||
</servlet>
|
||||
|
||||
<!-- servlet mappings -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>view</servlet-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
<servlet-mapping>
|
||||
<servlet-name>sse</servlet-name>
|
||||
<url-pattern>/events/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- context params -->
|
||||
<context-param>
|
||||
<param-name>webapp-slf4j-logger.format</param-name>
|
||||
<param-value>%logger [%level] [%ip] %message @%file:%line:%column</param-value>
|
||||
</context-param>
|
||||
</web-app>
|
@@ -0,0 +1,28 @@
|
||||
package org.jeudego.pairgoth.oauth
|
||||
|
||||
class FacebookHelper : OAuthHelper() {
|
||||
override val name: String
|
||||
get() = "facebook"
|
||||
|
||||
override fun getLoginURL(sessionId: String?): String {
|
||||
return "https://www.facebook.com/v14.0/dialog/oauth?" +
|
||||
"client_id=" + clientId +
|
||||
"&redirect_uri=" + redirectURI +
|
||||
"&scope=email" +
|
||||
"&state=" + getState(sessionId!!)
|
||||
}
|
||||
|
||||
override fun getAccessTokenURL(code: String): String? {
|
||||
return "https://graph.facebook.com/v14.0/oauth/access_token?" +
|
||||
"client_id=" + clientId +
|
||||
"&redirect_uri=" + redirectURI +
|
||||
"&client_secret=" + secret +
|
||||
"&code=" + code
|
||||
}
|
||||
|
||||
override fun getUserInfosURL(accessToken: String): String? {
|
||||
return "https://graph.facebook.com/me?" +
|
||||
"field=email" +
|
||||
"&access_token=" + accessToken
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package org.jeudego.pairgoth.oauth
|
||||
|
||||
class GoogleHelper : OAuthHelper() {
|
||||
override val name: String
|
||||
get() = "google"
|
||||
|
||||
override fun getLoginURL(sessionId: String?): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun getAccessTokenURL(code: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getUserInfosURL(accessToken: String): String? {
|
||||
return null
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package org.jeudego.pairgoth.oauth
|
||||
|
||||
class InstagramHelper : OAuthHelper() {
|
||||
override val name: String
|
||||
get() = "instagram"
|
||||
|
||||
override fun getLoginURL(sessionId: String?): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun getAccessTokenURL(code: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getUserInfosURL(accessToken: String): String? {
|
||||
return null
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
package org.jeudego.pairgoth.oauth
|
||||
|
||||
// In progress
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
//import com.republicate.modality.util.AESCryptograph
|
||||
//import com.republicate.modality.util.Cryptograph
|
||||
import org.apache.commons.codec.binary.Base64
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLEncoder
|
||||
|
||||
abstract class OAuthHelper {
|
||||
abstract val name: String
|
||||
abstract fun getLoginURL(sessionId: String?): String
|
||||
protected val clientId: String
|
||||
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".client_id")
|
||||
protected val secret: String
|
||||
protected get() = WebappManager.getMandatoryProperty("oauth." + name + ".secret")
|
||||
protected val redirectURI: String?
|
||||
protected get() = try {
|
||||
val uri: String = WebappManager.Companion.getProperty("webapp.url") + "/oauth.html"
|
||||
URLEncoder.encode(uri, "UTF-8")
|
||||
} catch (uee: UnsupportedEncodingException) {
|
||||
logger.error("could not encode redirect URI", uee)
|
||||
null
|
||||
}
|
||||
|
||||
protected fun getState(sessionId: String): String {
|
||||
return name + ":" + encrypt(sessionId)
|
||||
}
|
||||
|
||||
fun checkState(state: String, expectedSessionId: String): Boolean {
|
||||
val foundSessionId = decrypt(state)
|
||||
return expectedSessionId == foundSessionId
|
||||
}
|
||||
|
||||
protected abstract fun getAccessTokenURL(code: String): String?
|
||||
@Throws(IOException::class)
|
||||
fun getAccessToken(code: String): String {
|
||||
val json: Json.Object = Json.Object() // TODO - apiClient.get(getAccessTokenURL(code))
|
||||
return json.getString("access_token")!! // ?!
|
||||
}
|
||||
|
||||
protected abstract fun getUserInfosURL(accessToken: String): String?
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getUserEmail(accessToken: String): String {
|
||||
val json: Json.Object = Json.Object()
|
||||
// TODO
|
||||
// apiClient.get(getUserInfosURL(accessToken))
|
||||
return json.getString("email") ?: throw IOException("could not fetch email")
|
||||
}
|
||||
|
||||
companion object {
|
||||
protected var logger = LoggerFactory.getLogger("oauth")
|
||||
private const val salt = "0efd28fb53cbac42"
|
||||
// private val sessionIdCrypto: Cryptograph = AESCryptograph().apply {
|
||||
// init(salt)
|
||||
// }
|
||||
|
||||
private fun encrypt(input: String): String {
|
||||
return "TODO"
|
||||
// return Base64.encodeBase64URLSafeString(sessionIdCrypto.encrypt(input))
|
||||
}
|
||||
|
||||
private fun decrypt(input: String): String {
|
||||
return "TODO"
|
||||
// return sessionIdCrypto.decrypt(Base64.decodeBase64(input))
|
||||
}
|
||||
|
||||
// TODO
|
||||
// private val apiClient: ApiClient = ApiClient()
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package org.jeudego.pairgoth.oauth
|
||||
|
||||
object OauthHelperFactory {
|
||||
private val facebook: OAuthHelper = FacebookHelper()
|
||||
private val google: OAuthHelper = GoogleHelper()
|
||||
private val instagram: OAuthHelper = InstagramHelper()
|
||||
private val twitter: OAuthHelper = TwitterHelper()
|
||||
fun getHelper(provider: String?): OAuthHelper {
|
||||
return when (provider) {
|
||||
"facebook" -> facebook
|
||||
"google" -> google
|
||||
"instagram" -> instagram
|
||||
"twitter" -> twitter
|
||||
else -> throw RuntimeException("wrong provider")
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package org.jeudego.pairgoth.oauth
|
||||
|
||||
class TwitterHelper : OAuthHelper() {
|
||||
override val name: String
|
||||
get() = "twitter"
|
||||
|
||||
override fun getLoginURL(sessionId: String?): String {
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun getAccessTokenURL(code: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getUserInfosURL(accessToken: String): String? {
|
||||
return null
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
})
|
@@ -0,0 +1,20 @@
|
||||
package org.jeudego.pairgoth.util
|
||||
|
||||
import org.apache.velocity.Template
|
||||
import org.apache.velocity.exception.ResourceNotFoundException
|
||||
import org.apache.velocity.runtime.directive.Parse
|
||||
import org.jeudego.pairgoth.view.TranslationTool
|
||||
import org.jeudego.pairgoth.web.LanguageFilter
|
||||
|
||||
class TranslateDirective : Parse() {
|
||||
override fun getName(): String {
|
||||
return "translate"
|
||||
}
|
||||
|
||||
override fun getTemplate(path: String, encoding: String): Template? {
|
||||
val template = super.getTemplate(path, encoding)
|
||||
val translator = TranslationTool.translator.get()
|
||||
?: throw RuntimeException("no current active translator")
|
||||
return translator.translate(path, template)
|
||||
}
|
||||
}
|
@@ -0,0 +1,169 @@
|
||||
package org.jeudego.pairgoth.util
|
||||
|
||||
import org.apache.commons.lang3.StringEscapeUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.velocity.Template
|
||||
import org.apache.velocity.runtime.parser.node.ASTText
|
||||
import org.apache.velocity.runtime.parser.node.SimpleNode
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.io.path.readLines
|
||||
import kotlin.io.path.useDirectoryEntries
|
||||
|
||||
class Translator private constructor(private val iso: String) {
|
||||
|
||||
fun translate(enText: String) = translations[iso]?.get(enText) ?: enText.also {
|
||||
reportMissingTranslation(enText)
|
||||
}
|
||||
|
||||
fun translate(uri: String, template: Template): Template? {
|
||||
if (iso == "en") return template
|
||||
val key = Pair(uri, iso)
|
||||
var translated = translatedTemplates[key]
|
||||
if (translated != null && translated.lastModified < template.lastModified) {
|
||||
translatedTemplates.remove(key)
|
||||
translated = null
|
||||
}
|
||||
if (translated == null) {
|
||||
synchronized(translatedTemplates) {
|
||||
translated = translatedTemplates[key]
|
||||
if (translated == null) {
|
||||
translated = template.clone() as Template
|
||||
val data: SimpleNode = translated!!.data as SimpleNode
|
||||
translateNode(data)
|
||||
translatedTemplates[key] = translated!!
|
||||
}
|
||||
}
|
||||
}
|
||||
return translated
|
||||
}
|
||||
|
||||
private fun translateNode(node: SimpleNode, ignoringInput: String? = null): String? {
|
||||
var ignoring = ignoringInput
|
||||
if (node is ASTText) translateFragments(node.text, ignoring).let {
|
||||
node.text = it.first
|
||||
ignoring = it.second
|
||||
}
|
||||
else for (i in 0 until node.jjtGetNumChildren()) {
|
||||
ignoring = translateNode(node.jjtGetChild(i) as SimpleNode, ignoring)
|
||||
}
|
||||
return ignoring
|
||||
}
|
||||
|
||||
private fun translateFragments(text: String, ignoringInput: String?): Pair<String, String?> {
|
||||
var ignoring = ignoringInput
|
||||
val ignoreMap = buildIgnoreMap(text, ignoring).also {
|
||||
ignoring = it.second
|
||||
}.first
|
||||
val sw = StringWriter()
|
||||
val output = PrintWriter(sw)
|
||||
val matcher = textExtractor.matcher(text)
|
||||
var pos = 0
|
||||
while (matcher.find(pos)) {
|
||||
val start = matcher.start()
|
||||
val end = matcher.end()
|
||||
if (start > pos) output.print(text.substring(pos, start))
|
||||
val ignore: Boolean = ignoreMap.floorEntry(start).value
|
||||
if (ignore) output.print(text.substring(start, end)) else {
|
||||
var group = 1
|
||||
var groupStart = matcher.start(group)
|
||||
while (groupStart == -1 && group < matcher.groupCount()) groupStart = matcher.start(++group)
|
||||
if (groupStart == -1) throw RuntimeException("unexpected case")
|
||||
if (groupStart > start) output.print(text.substring(start, groupStart))
|
||||
val capture = matcher.group(group)
|
||||
var token: String = StringEscapeUtils.unescapeHtml4(capture)
|
||||
if (StringUtils.containsOnly(token, "\r\n\t -;:.\"/<>\u00A00123456789€!")) output.print(capture) else {
|
||||
token = normalize(token)
|
||||
token = translate(token)
|
||||
output.print(StringEscapeUtils.escapeHtml4(token))
|
||||
}
|
||||
val groupEnd = matcher.end(group)
|
||||
if (groupEnd < end) output.print(text.substring(groupEnd, end))
|
||||
}
|
||||
pos = end
|
||||
}
|
||||
if (pos < text.length) output.print(text.substring(pos))
|
||||
return Pair(sw.toString(), ignoring)
|
||||
}
|
||||
|
||||
private fun normalize(str: String): String {
|
||||
return str.replace(Regex("\\s+"), " ")
|
||||
}
|
||||
|
||||
private fun buildIgnoreMap(text: String, ignoringInput: String?): Pair<NavigableMap<Int, Boolean>, String?> {
|
||||
val map: NavigableMap<Int, Boolean> = TreeMap()
|
||||
var ignoring = ignoringInput
|
||||
var pos = 0
|
||||
map[0] = (ignoring != null)
|
||||
while (pos < text.length) {
|
||||
if (ignoring == null) {
|
||||
val nextIgnore = ignoredTags.map { tag ->
|
||||
Pair(tag, text.indexOf("<$tag(?:>\\s)"))
|
||||
}.filter {
|
||||
it.second != -1
|
||||
}.sortedBy {
|
||||
it.second
|
||||
}.firstOrNull()
|
||||
if (nextIgnore == null) pos = text.length
|
||||
else {
|
||||
ignoring = nextIgnore.first
|
||||
pos += nextIgnore.first.length + 2
|
||||
}
|
||||
} else {
|
||||
val closingTag = text.indexOf("</$ignoring>")
|
||||
if (closingTag == -1) pos = text.length
|
||||
else {
|
||||
pos += ignoring.length + 3
|
||||
ignoring = null
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair(map, ignoring)
|
||||
}
|
||||
|
||||
private var ASTText.text: String
|
||||
get() = textAccessor[this] as String
|
||||
set(value: String) { textAccessor[this] = value }
|
||||
|
||||
private fun reportMissingTranslation(enText: String) {
|
||||
logger.warn("missing translation towards {}: {}", iso, enText)
|
||||
// CB TODO - create file
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val textAccessor = ASTText::class.java.getDeclaredField("ctext").apply { isAccessible = true }
|
||||
private val logger = LoggerFactory.getLogger("translation")
|
||||
private val translatedTemplates: MutableMap<Pair<String, String>, Template> = ConcurrentHashMap<Pair<String, String>, Template>()
|
||||
private val textExtractor = Pattern.compile(
|
||||
"<[^>]+\\splaceholder=\"(?<placeholder>[^\"]*)\"[^>]*>|(?<=>)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<|$)|(?<=>|^)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?<text2>[^<>]+?)(?:[ \\r\\n\\t\u00A0/–-]| |‐)*(?=<)",
|
||||
Pattern.DOTALL
|
||||
)
|
||||
private val ignoredTags = setOf("head", "script", "style")
|
||||
|
||||
private val translations = Path.of(WebappManager.context.getRealPath("WEB-INF/translations")).useDirectoryEntries("??") { entries ->
|
||||
entries.map { file ->
|
||||
Pair(
|
||||
file.fileName.toString(),
|
||||
file.readLines(StandardCharsets.UTF_8).filter {
|
||||
it.isNotEmpty() && it.contains('\t') && !it.startsWith('#')
|
||||
}.map {
|
||||
Pair(it.substringBefore('\t'), it.substringAfter('\t'))
|
||||
}.toMap()
|
||||
)
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private val translators = ConcurrentHashMap<String, Translator>()
|
||||
fun getTranslator(iso: String) = translators.getOrPut(iso) { Translator(iso) }
|
||||
|
||||
val providedLanguages = setOf("en", "fr")
|
||||
const val defaultLanguage = "en"
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package org.jeudego.pairgoth.view
|
||||
|
||||
import org.apache.velocity.tools.config.ValidScope
|
||||
import org.jeudego.pairgoth.util.Translator
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
@ValidScope("request")
|
||||
class TranslationTool {
|
||||
|
||||
fun translate(enText: String): String {
|
||||
return translator.get().translate(enText)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val translator = ThreadLocal<Translator>()
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package org.jeudego.egc2024.web
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.servlet.Filter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.FilterConfig
|
||||
import javax.servlet.RequestDispatcher
|
||||
import javax.servlet.ServletException
|
||||
import javax.servlet.ServletRequest
|
||||
import javax.servlet.ServletResponse
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class DispatchingFilter : Filter {
|
||||
|
||||
protected val defaultRequestDispatcher: RequestDispatcher by lazy {
|
||||
filterConfig.servletContext.getNamedDispatcher("default")
|
||||
}
|
||||
|
||||
private lateinit var filterConfig: FilterConfig
|
||||
|
||||
override fun init(filterConfig: FilterConfig) {
|
||||
this.filterConfig = filterConfig
|
||||
}
|
||||
|
||||
override fun destroy() {}
|
||||
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
val req = request as HttpServletRequest
|
||||
val resp = response as HttpServletResponse
|
||||
val uri = req.requestURI
|
||||
when {
|
||||
uri.endsWith('/') -> response.sendRedirect("${uri}index")
|
||||
uri.contains('.') -> defaultRequestDispatcher.forward(request, response)
|
||||
else -> chain.doFilter(request, response)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
|
||||
import org.jeudego.pairgoth.util.Translator
|
||||
import org.jeudego.pairgoth.util.Translator.Companion.defaultLanguage
|
||||
import org.jeudego.pairgoth.util.Translator.Companion.getTranslator
|
||||
import org.jeudego.pairgoth.util.Translator.Companion.providedLanguages
|
||||
import org.jeudego.pairgoth.view.TranslationTool
|
||||
import javax.servlet.Filter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.FilterConfig
|
||||
import javax.servlet.ServletRequest
|
||||
import javax.servlet.ServletResponse
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class LanguageFilter : Filter {
|
||||
private var filterConfig: FilterConfig? = null
|
||||
|
||||
override fun init(filterConfig: FilterConfig) {
|
||||
this.filterConfig = filterConfig
|
||||
}
|
||||
|
||||
override fun doFilter(req: ServletRequest, resp: ServletResponse, chain: FilterChain) {
|
||||
val request = req as HttpServletRequest
|
||||
val response = resp as HttpServletResponse
|
||||
|
||||
val uri = request.requestURI
|
||||
val match = langPattern.matchEntire(uri)
|
||||
val lang = match?.groupValues?.get(1)
|
||||
val target = match?.groupValues?.get(2) ?: uri
|
||||
|
||||
if (lang != null && providedLanguages.contains(lang)) {
|
||||
// the target URI contains a language we provide
|
||||
request.setAttribute("lang", lang)
|
||||
request.setAttribute("target", target)
|
||||
TranslationTool.translator.set(Translator.getTranslator(lang))
|
||||
chain.doFilter(request, response)
|
||||
} else {
|
||||
// the request must be redirected
|
||||
val preferredLanguage = getPreferredLanguage(request)
|
||||
val destination = if (lang != null) target else uri
|
||||
response.sendRedirect("${preferredLanguage}${destination}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreferredLanguage(request: HttpServletRequest): String {
|
||||
return (request.session.getAttribute("lang") as String?) ?:
|
||||
( langHeaderParser.findAll(request.getHeader("Accept-Language") ?: "").filter {
|
||||
providedLanguages.contains(it.groupValues[1])
|
||||
}.sortedByDescending {
|
||||
it.groupValues[2].toDoubleOrNull() ?: 1.0
|
||||
}.firstOrNull()?.let {
|
||||
it.groupValues[1]
|
||||
} ?: defaultLanguage ).also {
|
||||
request.session.setAttribute("lang", it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroy() {}
|
||||
|
||||
companion object {
|
||||
private val langPattern = Regex("/([a-z]{2})(/.+)")
|
||||
private val langHeaderParser = Regex("(?:\\b(\\*|[a-z]{2})(?:_\\w+)?)(?:;q=([0-9.]+))?")
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
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
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
|
||||
import info.macias.sse.EventBroadcast
|
||||
import info.macias.sse.events.MessageEvent
|
||||
import info.macias.sse.servlet3.ServletEventTarget
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.servlet.http.HttpServlet
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
|
||||
class SSEServlet: HttpServlet() {
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger("sse")
|
||||
private var zeInstance: SSEServlet? = null
|
||||
internal fun getInstance(): SSEServlet = zeInstance ?: throw Error("SSE servlet not ready")
|
||||
}
|
||||
init {
|
||||
if (zeInstance != null) throw Error("Multiple instances of SSE servlet found!")
|
||||
zeInstance = this
|
||||
}
|
||||
private val broadcast = EventBroadcast()
|
||||
|
||||
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse?) {
|
||||
logger.trace("<< new channel")
|
||||
broadcast.addSubscriber(ServletEventTarget(req), req.getHeader("Last-Event-Id"))
|
||||
}
|
||||
|
||||
internal fun broadcast(message: MessageEvent) = broadcast.broadcast(message)
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
package org.jeudego.pairgoth.web
|
||||
|
||||
import org.apache.commons.lang3.tuple.Pair
|
||||
import org.apache.velocity.Template
|
||||
import org.apache.velocity.context.Context
|
||||
import org.apache.velocity.tools.view.ServletUtils
|
||||
import org.apache.velocity.tools.view.VelocityViewServlet
|
||||
import org.jeudego.pairgoth.util.Translator
|
||||
import org.jeudego.pairgoth.web.WebappManager
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
import java.io.Serializable
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.function.Function
|
||||
import java.util.stream.Collectors
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
class ViewServlet : VelocityViewServlet() {
|
||||
private fun fileExists(path: String): Boolean {
|
||||
return File(servletContext.getRealPath(path)).exists()
|
||||
}
|
||||
|
||||
private fun decodeURI(request: HttpServletRequest): String {
|
||||
var uri = request.requestURI
|
||||
uri = try {
|
||||
URLDecoder.decode(uri, "UTF-8")
|
||||
} catch (use: UnsupportedEncodingException) {
|
||||
throw RuntimeException("could not decode URI $uri", use)
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
override fun getTemplate(request: HttpServletRequest, response: HttpServletResponse?): Template = getTemplate(STANDARD_LAYOUT)
|
||||
|
||||
override fun fillContext(context: Context, request: HttpServletRequest) {
|
||||
super.fillContext(context, request)
|
||||
var uri = decodeURI(request)
|
||||
context.put("page", uri)
|
||||
val base = uri.replaceFirst(".html$".toRegex(), "")
|
||||
val suffixes = Arrays.asList("js", "css")
|
||||
for (suffix in suffixes) {
|
||||
val resource = "/$suffix$base.$suffix"
|
||||
if (fileExists(resource)) {
|
||||
context.put(suffix, resource)
|
||||
}
|
||||
}
|
||||
val lang = request.getAttribute("lang") as String
|
||||
/*
|
||||
val menu = menuEntries!![uri]
|
||||
var title: String? = null
|
||||
if (lang != null && menu != null) title = menu.getString(lang)
|
||||
if (title != null) context.put("title", title)
|
||||
if (lang != null) context.put(
|
||||
"dateformat",
|
||||
DateFormat.getDateInstance(DateFormat.LONG, Locale.forLanguageTag(lang))
|
||||
)
|
||||
*/
|
||||
}
|
||||
|
||||
override fun error(
|
||||
request: HttpServletRequest?,
|
||||
response: HttpServletResponse,
|
||||
e: Throwable?
|
||||
) {
|
||||
val path: String = ServletUtils.getPath(request)
|
||||
if (response.isCommitted) {
|
||||
log.error("An error occured but the response headers have already been sent.")
|
||||
log.error("Error processing a template for path '{}'", path, e)
|
||||
return
|
||||
}
|
||||
try {
|
||||
log.error("Error processing a template for path '{}'", path, e)
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
|
||||
} catch (e2: Exception) {
|
||||
// clearly something is quite wrong.
|
||||
// let's log the new exception then give up and
|
||||
// throw a runtime exception that wraps the first one
|
||||
val msg = "Exception while printing error screen"
|
||||
log.error(msg, e2)
|
||||
throw RuntimeException(msg, e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STANDARD_LAYOUT = "/WEB-INF/layouts/standard.html"
|
||||
}
|
||||
}
|
@@ -0,0 +1,167 @@
|
||||
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
|
||||
|
||||
@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<TrustManager>(object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate>? {
|
||||
return null
|
||||
}
|
||||
|
||||
@Suppress("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
|
||||
@Suppress("TrustAllX509TrustManager")
|
||||
override fun checkServerTrusted(certs: Array<X509Certificate>, 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)
|
||||
}
|
||||
}
|
||||
|
||||
/* ServletContextListener interface */
|
||||
override fun contextInitialized(sce: ServletContextEvent) {
|
||||
context = sce.servletContext
|
||||
logger.info("---------- Starting Web Application ----------")
|
||||
context.setAttribute("manager", this)
|
||||
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
|
||||
}
|
||||
|
||||
logger.info("Using profile {}", properties.getProperty("webapp.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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun contextDestroyed(sce: ServletContextEvent) {
|
||||
logger.info("---------- Stopping Web Application ----------")
|
||||
|
||||
val context = sce.servletContext
|
||||
for (service in webServices.keys) stopService(service, true)
|
||||
// ??? DriverManager.deregisterDriver(com.mysql.cj.jdbc.Driver ...);
|
||||
}
|
||||
|
||||
/* 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
|
||||
private val webServices: MutableMap<String?, Pair<Runnable, Thread?>> = 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 properties.getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||
}
|
||||
|
||||
val webappURL by lazy { getProperty("webapp.url") }
|
||||
|
||||
private val services = mutableMapOf<String, Pair<Runnable, Thread>>()
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
0
view-webapp/src/test/kotlin/.gitkeep
Normal file
0
view-webapp/src/test/kotlin/.gitkeep
Normal file
24
view-webapp/src/test/kotlin/TestBase.kt
Normal file
24
view-webapp/src/test/kotlin/TestBase.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package org.jeudego.pairgoth.test
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.TestInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
|
||||
abstract class TestBase {
|
||||
companion object {
|
||||
val logger = LoggerFactory.getLogger("test")
|
||||
|
||||
@BeforeAll
|
||||
@JvmStatic
|
||||
fun prepare() {
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun before(testInfo: TestInfo) {
|
||||
val testName = testInfo.displayName.removeSuffix("()")
|
||||
logger.info("===== Running $testName =====")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user