From 241d95fe1c00b0d687c3086f4f34461973473f67 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Thu, 16 Jan 2025 21:11:35 +0800 Subject: [PATCH] added env variable and java properties substitution in configuration attributes --- .../api/exception/ConfigurationException.java | 11 +++++ gbcs-base/build.gradle | 5 +- gbcs-base/src/main/java/module-info.java | 1 + .../main/kotlin/net/woggioni/gbcs/base/Xml.kt | 22 +++++++++ gbcs-cli/build.gradle | 9 ++++ gbcs-client/build.gradle | 1 + gbcs-client/src/main/java/module-info.java | 1 + .../net/woggioni/gbcs/client/impl/Parser.kt | 21 ++++---- .../gbcs/client/schema/gbcs-client.xsd | 4 +- .../gbcs/memcached/MemcachedCacheProvider.kt | 21 ++++---- .../gbcs/memcached/schema/gbcs-memcached.xsd | 2 +- .../gbcs/cache/FileSystemCacheProvider.kt | 15 +++--- .../net/woggioni/gbcs/configuration/Parser.kt | 48 +++++++++---------- .../net/woggioni/gbcs/schema/gbcs.xsd | 12 ++--- 14 files changed, 105 insertions(+), 68 deletions(-) create mode 100644 gbcs-api/src/main/java/net/woggioni/gbcs/api/exception/ConfigurationException.java diff --git a/gbcs-api/src/main/java/net/woggioni/gbcs/api/exception/ConfigurationException.java b/gbcs-api/src/main/java/net/woggioni/gbcs/api/exception/ConfigurationException.java new file mode 100644 index 0000000..9f722a6 --- /dev/null +++ b/gbcs-api/src/main/java/net/woggioni/gbcs/api/exception/ConfigurationException.java @@ -0,0 +1,11 @@ +package net.woggioni.gbcs.api.exception; + +public class ConfigurationException extends GbcsException { + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigurationException(String message) { + this(message, null); + } +} diff --git a/gbcs-base/build.gradle b/gbcs-base/build.gradle index 073a216..651b091 100644 --- a/gbcs-base/build.gradle +++ b/gbcs-base/build.gradle @@ -6,8 +6,9 @@ plugins { } dependencies { - compileOnly project(':gbcs-api') - compileOnly catalog.slf4j.api + implementation project(':gbcs-api') + implementation catalog.slf4j.api + implementation catalog.jwo } publishing { diff --git a/gbcs-base/src/main/java/module-info.java b/gbcs-base/src/main/java/module-info.java index 2fa3505..7ebf117 100644 --- a/gbcs-base/src/main/java/module-info.java +++ b/gbcs-base/src/main/java/module-info.java @@ -3,6 +3,7 @@ module net.woggioni.gbcs.base { requires java.logging; requires org.slf4j; requires kotlin.stdlib; + requires net.woggioni.jwo; exports net.woggioni.gbcs.base; } \ No newline at end of file diff --git a/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt index e4dcc39..1ad00f3 100644 --- a/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt +++ b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/Xml.kt @@ -1,5 +1,9 @@ package net.woggioni.gbcs.base +import net.woggioni.jwo.CollectionUtils.mapValues +import net.woggioni.jwo.CollectionUtils.toUnmodifiableTreeMap +import net.woggioni.jwo.JWO +import net.woggioni.jwo.MapBuilder import org.slf4j.LoggerFactory import org.slf4j.event.Level import org.w3c.dom.Document @@ -12,6 +16,8 @@ import org.xml.sax.SAXParseException import java.io.InputStream import java.io.OutputStream import java.net.URL +import java.util.Collections +import java.util.TreeMap import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING @@ -95,6 +101,22 @@ class Xml(val doc: Document, val element: Element) { } companion object { + private val dictMap: Map> = sequenceOf( + "env" to System.getenv().asSequence().map { (k, v) -> k to (v as Any) }.toMap(), + "sys" to System.getProperties().asSequence().map { (k, v) -> k as String to (v as Any) }.toMap() + ).toMap() + + private fun renderConfigurationTemplate(template: String): String { + return JWO.renderTemplate(template, emptyMap(), dictMap) + } + + fun Element.renderAttribute(name : String, namespaceURI: String? = null) = if(namespaceURI == null) { + getAttribute(name) + } else { + getAttributeNS(name, namespaceURI) + }.takeIf(String::isNotEmpty)?.let(Companion::renderConfigurationTemplate) + + fun Element.asIterable() = Iterable { ElementIterator(this, null) } fun NodeList.asIterable() = Iterable { NodeListIterator(this) } diff --git a/gbcs-cli/build.gradle b/gbcs-cli/build.gradle index dbd38ce..38afb4c 100644 --- a/gbcs-cli/build.gradle +++ b/gbcs-cli/build.gradle @@ -4,6 +4,8 @@ plugins { alias catalog.plugins.envelope alias catalog.plugins.sambal alias catalog.plugins.graalvm.native.image + alias catalog.plugins.graalvm.jlink + alias catalog.plugins.jpms.check id 'maven-publish' } @@ -12,6 +14,8 @@ import net.woggioni.gradle.envelope.EnvelopeJarTask import net.woggioni.gradle.graalvm.NativeImageConfigurationTask import net.woggioni.gradle.graalvm.NativeImagePlugin import net.woggioni.gradle.graalvm.NativeImageTask +import net.woggioni.gradle.graalvm.JlinkPlugin +import net.woggioni.gradle.graalvm.JlinkTask Property mainClassName = objects.property(String.class) mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli') @@ -65,6 +69,11 @@ tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) { buildStaticImage = true } +tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) { + mainClass = mainClassName + mainModule = 'net.woggioni.gbcs.cli' +} + artifacts { release(envelopeJarTaskProvider) } diff --git a/gbcs-client/build.gradle b/gbcs-client/build.gradle index b1bf6e2..baae865 100644 --- a/gbcs-client/build.gradle +++ b/gbcs-client/build.gradle @@ -4,6 +4,7 @@ plugins { } dependencies { + implementation project(':gbcs-api') implementation project(':gbcs-base') implementation catalog.picocli implementation catalog.slf4j.api diff --git a/gbcs-client/src/main/java/module-info.java b/gbcs-client/src/main/java/module-info.java index 7fe1e75..1becc63 100644 --- a/gbcs-client/src/main/java/module-info.java +++ b/gbcs-client/src/main/java/module-info.java @@ -7,6 +7,7 @@ module net.woggioni.gbcs.client { requires io.netty.buffer; requires java.xml; requires net.woggioni.gbcs.base; + requires net.woggioni.gbcs.api; requires io.netty.codec; requires org.slf4j; diff --git a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt index 878c90c..b6daca4 100644 --- a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt +++ b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt @@ -1,6 +1,8 @@ package net.woggioni.gbcs.client.impl +import net.woggioni.gbcs.api.exception.ConfigurationException import net.woggioni.gbcs.base.Xml.Companion.asIterable +import net.woggioni.gbcs.base.Xml.Companion.renderAttribute import net.woggioni.gbcs.client.GbcsClient import org.w3c.dom.Document import java.net.URI @@ -21,17 +23,17 @@ object Parser { val tagName = child.localName when (tagName) { "profile" -> { - val name = child.getAttribute("name") - val uri = child.getAttribute("base-url").let(::URI) + val name = child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required") + val uri = child.renderAttribute("base-url")?.let(::URI) ?: throw ConfigurationException("base-url attribute is required") var authentication: GbcsClient.Configuration.Authentication? = null for (gchild in child.asIterable()) { when (gchild.localName) { "tls-client-auth" -> { - val keyStoreFile = gchild.getAttribute("key-store-file") + val keyStoreFile = gchild.renderAttribute("key-store-file") val keyStorePassword = - gchild.getAttribute("key-store-password").takeIf(String::isNotEmpty) - val keyAlias = gchild.getAttribute("key-alias") - val keyPassword = gchild.getAttribute("key-password").takeIf(String::isNotEmpty) + gchild.renderAttribute("key-store-password") + val keyAlias = gchild.renderAttribute("key-alias") + val keyPassword = gchild.renderAttribute("key-password") val keystore = KeyStore.getInstance("PKCS12").apply { Files.newInputStream(Path.of(keyStoreFile)).use { @@ -48,15 +50,14 @@ object Parser { } "basic-auth" -> { - val username = gchild.getAttribute("user") - val password = gchild.getAttribute("password") + val username = gchild.renderAttribute("user") ?: throw ConfigurationException("username attribute is required") + val password = gchild.renderAttribute("password") ?: throw ConfigurationException("password attribute is required") authentication = GbcsClient.Configuration.Authentication.BasicAuthenticationCredentials(username, password) } } } - val maxConnections = child.getAttribute("max-connections") - .takeIf(String::isNotEmpty) + val maxConnections = child.renderAttribute("max-connections") ?.let(String::toInt) ?: 50 profiles[name] = GbcsClient.Configuration.Profile(uri, authentication, maxConnections) diff --git a/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd b/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd index f98dac5..6add7eb 100644 --- a/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd +++ b/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd @@ -23,14 +23,14 @@ - + - + diff --git a/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt b/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt index f82659d..ba99a25 100644 --- a/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt +++ b/gbcs-memcached/src/main/kotlin/net/woggioni/gbcs/memcached/MemcachedCacheProvider.kt @@ -2,10 +2,12 @@ package net.woggioni.gbcs.memcached import net.rubyeye.xmemcached.transcoders.CompressionMode import net.woggioni.gbcs.api.CacheProvider +import net.woggioni.gbcs.api.exception.ConfigurationException import net.woggioni.gbcs.base.GBCS import net.woggioni.gbcs.base.HostAndPort import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.base.Xml.Companion.asIterable +import net.woggioni.gbcs.base.Xml.Companion.renderAttribute import org.w3c.dom.Document import org.w3c.dom.Element import java.time.Duration @@ -22,20 +24,13 @@ class MemcachedCacheProvider : CacheProvider { override fun deserialize(el: Element): MemcachedCacheConfiguration { val servers = mutableListOf() - val maxAge = el.getAttribute("max-age") - .takeIf(String::isNotEmpty) + val maxAge = el.renderAttribute("max-age") ?.let(Duration::parse) ?: Duration.ofDays(1) - val maxSize = el.getAttribute("max-size") - .takeIf(String::isNotEmpty) + val maxSize = el.renderAttribute("max-size") ?.let(String::toInt) ?: 0x100000 - val enableCompression = el.getAttribute("enable-compression") - .takeIf(String::isNotEmpty) - ?.let(String::toBoolean) - ?: false - val compressionMode = el.getAttribute("compression-mode") - .takeIf(String::isNotEmpty) + val compressionMode = el.renderAttribute("compression-mode") ?.let { when (it) { "gzip" -> CompressionMode.GZIP @@ -44,11 +39,13 @@ class MemcachedCacheProvider : CacheProvider { } } ?: CompressionMode.ZIP - val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) + val digestAlgorithm = el.renderAttribute("digest") for (child in el.asIterable()) { when (child.nodeName) { "server" -> { - servers.add(HostAndPort(child.getAttribute("host"), child.getAttribute("port").toInt())) + val host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required") + val port = child.renderAttribute("port")?.toInt() ?: throw ConfigurationException("port attribute is required") + servers.add(HostAndPort(host, port)) } } } diff --git a/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd b/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd index 32af191..eb3dc48 100644 --- a/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd +++ b/gbcs-memcached/src/main/resources/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd @@ -7,7 +7,7 @@ - + diff --git a/src/main/kotlin/net/woggioni/gbcs/cache/FileSystemCacheProvider.kt b/src/main/kotlin/net/woggioni/gbcs/cache/FileSystemCacheProvider.kt index 26ff143..04a6abe 100644 --- a/src/main/kotlin/net/woggioni/gbcs/cache/FileSystemCacheProvider.kt +++ b/src/main/kotlin/net/woggioni/gbcs/cache/FileSystemCacheProvider.kt @@ -3,6 +3,7 @@ package net.woggioni.gbcs.cache import net.woggioni.gbcs.api.CacheProvider import net.woggioni.gbcs.base.GBCS import net.woggioni.gbcs.base.Xml +import net.woggioni.gbcs.base.Xml.Companion.renderAttribute import org.w3c.dom.Document import org.w3c.dom.Element import java.nio.file.Path @@ -18,22 +19,18 @@ class FileSystemCacheProvider : CacheProvider { override fun getXmlNamespace() = "urn:net.woggioni.gbcs" override fun deserialize(el: Element): FileSystemCacheConfiguration { - val path = el.getAttribute("path") - .takeIf(String::isNotEmpty) + val path = el.renderAttribute("path") ?.let(Path::of) - val maxAge = el.getAttribute("max-age") - .takeIf(String::isNotEmpty) + val maxAge = el.renderAttribute("max-age") ?.let(Duration::parse) ?: Duration.ofDays(1) - val enableCompression = el.getAttribute("enable-compression") - .takeIf(String::isNotEmpty) + val enableCompression = el.renderAttribute("enable-compression") ?.let(String::toBoolean) ?: true - val compressionLevel = el.getAttribute("compression-level") - .takeIf(String::isNotEmpty) + val compressionLevel = el.renderAttribute("compression-level") ?.let(String::toInt) ?: Deflater.DEFAULT_COMPRESSION - val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) ?: "MD5" + val digestAlgorithm = el.renderAttribute("digest") ?: "MD5" return FileSystemCacheConfiguration( path, diff --git a/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt b/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt index d969838..7acf760 100644 --- a/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt +++ b/src/main/kotlin/net/woggioni/gbcs/configuration/Parser.kt @@ -12,7 +12,9 @@ import net.woggioni.gbcs.api.Configuration.TlsCertificateExtractor import net.woggioni.gbcs.api.Configuration.TrustStore import net.woggioni.gbcs.api.Configuration.User import net.woggioni.gbcs.api.Role +import net.woggioni.gbcs.api.exception.ConfigurationException import net.woggioni.gbcs.base.Xml.Companion.asIterable +import net.woggioni.gbcs.base.Xml.Companion.renderAttribute import org.w3c.dom.Document import org.w3c.dom.Element import org.w3c.dom.TypeInfo @@ -28,9 +30,8 @@ object Parser { var users : Map = mapOf(anonymousUser.name to anonymousUser) var groups = emptyMap() var tls: Tls? = null - val serverPath = root.getAttribute("path") - val useVirtualThread = root.getAttribute("useVirtualThreads") - .takeIf(String::isNotEmpty) + val serverPath = root.renderAttribute("path") + val useVirtualThread = root.renderAttribute("useVirtualThreads") ?.let(String::toBoolean) ?: true var authentication: Authentication? = null for (child in root.asIterable()) { @@ -53,8 +54,8 @@ object Parser { } "bind" -> { - host = child.getAttribute("host") - port = Integer.parseInt(child.getAttribute("port")) + host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required") + port = Integer.parseInt(child.renderAttribute("port")) } "cache" -> { @@ -79,14 +80,14 @@ object Parser { for (ggchild in gchild.asIterable()) { when (ggchild.localName) { "group-extractor" -> { - val attrName = ggchild.getAttribute("attribute-name") - val pattern = ggchild.getAttribute("pattern") + val attrName = ggchild.renderAttribute("attribute-name") + val pattern = ggchild.renderAttribute("pattern") tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern) } "user-extractor" -> { - val attrName = ggchild.getAttribute("attribute-name") - val pattern = ggchild.getAttribute("pattern") + val attrName = ggchild.renderAttribute("attribute-name") + val pattern = ggchild.renderAttribute("pattern") tlsExtractorUser = TlsCertificateExtractor(attrName, pattern) } } @@ -98,20 +99,17 @@ object Parser { } "tls" -> { - val verifyClients = child.getAttribute("verify-clients") - .takeIf(String::isNotEmpty) + val verifyClients = child.renderAttribute("verify-clients") ?.let(String::toBoolean) ?: false var keyStore: KeyStore? = null var trustStore: TrustStore? = null for (granChild in child.asIterable()) { when (granChild.localName) { "keystore" -> { - val keyStoreFile = Paths.get(granChild.getAttribute("file")) - val keyStorePassword = granChild.getAttribute("password") - .takeIf(String::isNotEmpty) - val keyAlias = granChild.getAttribute("key-alias") - val keyPassword = granChild.getAttribute("key-password") - .takeIf(String::isNotEmpty) + val keyStoreFile = Paths.get(granChild.renderAttribute("file")) + val keyStorePassword = granChild.renderAttribute("password") + val keyAlias = granChild.renderAttribute("key-alias") + val keyPassword = granChild.renderAttribute("key-password") keyStore = KeyStore( keyStoreFile, keyStorePassword, @@ -121,11 +119,9 @@ object Parser { } "truststore" -> { - val trustStoreFile = Paths.get(granChild.getAttribute("file")) - val trustStorePassword = granChild.getAttribute("password") - .takeIf(String::isNotEmpty) - val checkCertificateStatus = granChild.getAttribute("check-certificate-status") - .takeIf(String::isNotEmpty) + val trustStoreFile = Paths.get(granChild.renderAttribute("file")) + val trustStorePassword = granChild.renderAttribute("password") + val checkCertificateStatus = granChild.renderAttribute("check-certificate-status") ?.let(String::toBoolean) ?: false trustStore = TrustStore( @@ -152,15 +148,15 @@ object Parser { }.toSet() private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map { - it.getAttribute("ref") + it.renderAttribute("ref") }.toSet() private fun parseUsers(root: Element): Sequence { return root.asIterable().asSequence().filter { it.localName == "user" }.map { el -> - val username = el.getAttribute("name") - val password = el.getAttribute("password").takeIf(String::isNotEmpty) + val username = el.renderAttribute("name") + val password = el.renderAttribute("password") User(username, password, emptySet()) } } @@ -171,7 +167,7 @@ object Parser { val groups = root.asIterable().asSequence().filter { it.localName == "group" }.map { el -> - val groupName = el.getAttribute("name") + val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required") var roles = emptySet() for (child in el.asIterable()) { when (child.localName) { diff --git a/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd b/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd index f32e6dd..3991270 100644 --- a/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd +++ b/src/main/resources/net/woggioni/gbcs/schema/gbcs.xsd @@ -28,7 +28,7 @@ - + @@ -54,8 +54,8 @@ - - + + @@ -85,7 +85,7 @@ - + @@ -105,11 +105,11 @@ - + - +