Add pairgoth-common module to share sources between api and view webapps
This commit is contained in:
@@ -137,6 +137,11 @@
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jeudego.pairgoth</groupId>
|
||||
<artifactId>pairgoth-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<!-- main dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
|
@@ -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 {
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -1,57 +0,0 @@
|
||||
package org.jeudego.pairgoth.util
|
||||
|
||||
import java.nio.charset.Charset
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Cipher.DECRYPT_MODE
|
||||
import javax.crypto.Cipher.ENCRYPT_MODE
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
/**
|
||||
* Basic AES encryption. Please note that it uses the ECB block mode, which has the advantage
|
||||
* to not require random bytes, thus providing some *persistence* for the encrypted data, but
|
||||
* at the expense of some security weaknesses. The purpose here is just to encrypt temporary
|
||||
* session ids in URLs, not to protect state secrets.
|
||||
*/
|
||||
class AESCryptograph : Cryptograph {
|
||||
|
||||
override fun init(key: String) {
|
||||
val bytes = key.toByteArray(Charset.defaultCharset())
|
||||
if (bytes.size < 16) {
|
||||
throw Error("not enough secret bytes")
|
||||
}
|
||||
val secret: SecretKey = SecretKeySpec(bytes, 0, 16, ALGORITHM)
|
||||
try {
|
||||
encrypt.init(ENCRYPT_MODE, secret)
|
||||
decrypt.init(DECRYPT_MODE, secret)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("cyptograph initialization failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun encrypt(str: String): ByteArray {
|
||||
return try {
|
||||
encrypt.doFinal(str.toByteArray(Charset.defaultCharset()))
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("encryption failed failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun decrypt(bytes: ByteArray): String {
|
||||
return try {
|
||||
String(decrypt.doFinal(bytes), Charset.defaultCharset())
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("encryption failed failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
private var encrypt = Cipher.getInstance(CIPHER)
|
||||
private var decrypt = Cipher.getInstance(CIPHER)
|
||||
|
||||
companion object {
|
||||
private val CIPHER = "AES/ECB/PKCS5Padding"
|
||||
private val ALGORITHM = "AES"
|
||||
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -1,29 +0,0 @@
|
||||
package org.jeudego.pairgoth.util
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Cryptograph - used to encrypt and decrypt strings.
|
||||
*
|
||||
*/
|
||||
interface Cryptograph : Serializable {
|
||||
/**
|
||||
* init.
|
||||
* @param random random string
|
||||
*/
|
||||
fun init(random: String)
|
||||
|
||||
/**
|
||||
* encrypt.
|
||||
* @param str string to encrypt
|
||||
* @return encrypted string
|
||||
*/
|
||||
fun encrypt(str: String): ByteArray
|
||||
|
||||
/**
|
||||
* decrypt.
|
||||
* @param bytes to decrypt
|
||||
* @return decrypted string
|
||||
*/
|
||||
fun decrypt(bytes: ByteArray): String
|
||||
}
|
@@ -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
|
||||
}
|
||||
})
|
@@ -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")
|
||||
|
@@ -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")
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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<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)
|
||||
}
|
||||
}
|
||||
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<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 getProperty(prop) ?: throw Error("missing property: ${prop}")
|
||||
}
|
||||
|
||||
val webappURL by lazy { getProperty("webapp.external.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)
|
||||
}
|
||||
}
|
||||
val properties get() = BaseWebappManager.properties
|
||||
val context get() = BaseWebappManager.context
|
||||
fun getMandatoryProperty(prop: String) = properties.getProperty(prop) ?: throw Error("missing property: $prop")
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user