forked from woggioni/rbcs
ee7bc7e850
- Update lys.version to 2026.04.14 - Add optional compileOnly dependency on opentelemetry-netty-4.1 in rbcs-server - Add runtime guard to only activate instrumentation when OTel classes are on classpath - Insert OTel combined handler after HttpServerCodec in the Netty pipeline - Add requires-static JPMS directives for optional module support - Add enableTelemetry config attribute to rbcs:server with default false - Update Configuration DTO, XSD schema, Parser, Serializer, and all tests
361 lines
17 KiB
Kotlin
361 lines
17 KiB
Kotlin
package net.woggioni.rbcs.server.configuration
|
|
|
|
import java.nio.file.Paths
|
|
import java.time.Duration
|
|
import java.time.temporal.ChronoUnit
|
|
import net.woggioni.rbcs.api.Configuration
|
|
import net.woggioni.rbcs.api.Configuration.Authentication
|
|
import net.woggioni.rbcs.api.Configuration.BasicAuthentication
|
|
import net.woggioni.rbcs.api.Configuration.Cache
|
|
import net.woggioni.rbcs.api.Configuration.ClientCertificateAuthentication
|
|
import net.woggioni.rbcs.api.Configuration.ForwardedClientCertificateAuthentication
|
|
import net.woggioni.rbcs.api.Configuration.Group
|
|
import net.woggioni.rbcs.api.Configuration.KeyStore
|
|
import net.woggioni.rbcs.api.Configuration.Tls
|
|
import net.woggioni.rbcs.api.Configuration.TlsCertificateExtractor
|
|
import net.woggioni.rbcs.api.Configuration.TrustStore
|
|
import net.woggioni.rbcs.api.Configuration.User
|
|
import net.woggioni.rbcs.api.Role
|
|
import net.woggioni.rbcs.api.exception.ConfigurationException
|
|
import net.woggioni.rbcs.common.Cidr
|
|
import net.woggioni.rbcs.common.Xml.Companion.asIterable
|
|
import net.woggioni.rbcs.common.Xml.Companion.renderAttribute
|
|
import org.w3c.dom.Document
|
|
import org.w3c.dom.Element
|
|
import org.w3c.dom.TypeInfo
|
|
|
|
object Parser {
|
|
fun parse(document: Document): Configuration {
|
|
val root = document.documentElement
|
|
val anonymousUser = User("", null, emptySet(), null)
|
|
var connection: Configuration.Connection = Configuration.Connection(
|
|
Duration.of(30, ChronoUnit.SECONDS),
|
|
Duration.of(60, ChronoUnit.SECONDS),
|
|
Duration.of(60, ChronoUnit.SECONDS),
|
|
0x4000000,
|
|
0x10000
|
|
)
|
|
var rateLimiter = Configuration.RateLimiter(false, 0x100000, 100)
|
|
var eventExecutor: Configuration.EventExecutor = Configuration.EventExecutor(true)
|
|
var cache: Cache? = null
|
|
var host = "127.0.0.1"
|
|
var port = 11080
|
|
var proxyProtocolEnabled = false
|
|
var trustedProxies = emptyList<Cidr>()
|
|
var users: Map<String, User> = mapOf(anonymousUser.name to anonymousUser)
|
|
var groups = emptyMap<String, Group>()
|
|
var tls: Tls? = null
|
|
val serverPath = root.renderAttribute("path")
|
|
var incomingConnectionsBacklogSize = 1024
|
|
var authentication: Authentication? = null
|
|
for (child in root.asIterable()) {
|
|
val tagName = child.localName
|
|
when (tagName) {
|
|
"authentication" -> {
|
|
for (gchild in child.asIterable()) {
|
|
when (gchild.localName) {
|
|
"basic" -> {
|
|
authentication = BasicAuthentication()
|
|
}
|
|
|
|
"client-certificate" -> {
|
|
var tlsExtractorUser: TlsCertificateExtractor? = null
|
|
var tlsExtractorGroup: TlsCertificateExtractor? = null
|
|
for (ggchild in gchild.asIterable()) {
|
|
when (ggchild.localName) {
|
|
"group-extractor" -> {
|
|
val attrName = ggchild.renderAttribute("attribute-name")
|
|
val pattern = ggchild.renderAttribute("pattern")
|
|
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
|
|
}
|
|
|
|
"user-extractor" -> {
|
|
val attrName = ggchild.renderAttribute("attribute-name")
|
|
val pattern = ggchild.renderAttribute("pattern")
|
|
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
|
|
}
|
|
}
|
|
}
|
|
authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
|
|
}
|
|
|
|
"forwarded-client-certificate" -> {
|
|
val headerName = gchild.renderAttribute("header-name") ?: "X-Client-Cert-Subject-DN"
|
|
var tlsExtractorUser: TlsCertificateExtractor? = null
|
|
var tlsExtractorGroup: TlsCertificateExtractor? = null
|
|
for (ggchild in gchild.asIterable()) {
|
|
when (ggchild.localName) {
|
|
"group-extractor" -> {
|
|
val attrName = ggchild.renderAttribute("attribute-name")
|
|
val pattern = ggchild.renderAttribute("pattern")
|
|
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
|
|
}
|
|
|
|
"user-extractor" -> {
|
|
val attrName = ggchild.renderAttribute("attribute-name")
|
|
val pattern = ggchild.renderAttribute("pattern")
|
|
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
|
|
}
|
|
}
|
|
}
|
|
authentication = ForwardedClientCertificateAuthentication(headerName, tlsExtractorUser, tlsExtractorGroup)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
"authorization" -> {
|
|
var knownUsers = sequenceOf(anonymousUser)
|
|
for (gchild in child.asIterable()) {
|
|
when (gchild.localName) {
|
|
"users" -> {
|
|
knownUsers += parseUsers(gchild)
|
|
}
|
|
|
|
"groups" -> {
|
|
val pair = parseGroups(gchild, knownUsers)
|
|
users = pair.first
|
|
groups = pair.second
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
"bind" -> {
|
|
host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
|
|
port = Integer.parseInt(child.renderAttribute("port"))
|
|
proxyProtocolEnabled = child.renderAttribute("proxy-protocol")
|
|
?.let(String::toBoolean) ?: false
|
|
incomingConnectionsBacklogSize = child.renderAttribute("incoming-connections-backlog-size")
|
|
?.let(Integer::parseInt)
|
|
?: 1024
|
|
|
|
for(grandChild in child.asIterable()) {
|
|
when(grandChild.localName) {
|
|
"trusted-proxies" -> {
|
|
trustedProxies = parseTrustedProxies(grandChild)
|
|
}
|
|
}
|
|
}
|
|
child.asIterable().filter {
|
|
it.localName == "trusted-proxies"
|
|
}.firstOrNull()?.let(::parseTrustedProxies)
|
|
|
|
}
|
|
|
|
"cache" -> {
|
|
cache = (child as TypeInfo).let { tf ->
|
|
val typeNamespace = tf.typeNamespace
|
|
val typeName = tf.typeName
|
|
CacheSerializers.index[typeNamespace to typeName]
|
|
?: throw IllegalArgumentException("Cache provider for namespace '$typeNamespace' with name '$typeName' not found")
|
|
}.deserialize(child)
|
|
}
|
|
|
|
"connection" -> {
|
|
val idleTimeout = child.renderAttribute("idle-timeout")
|
|
?.let(Duration::parse) ?: Duration.of(30, ChronoUnit.SECONDS)
|
|
val readIdleTimeout = child.renderAttribute("read-idle-timeout")
|
|
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
|
val writeIdleTimeout = child.renderAttribute("write-idle-timeout")
|
|
?.let(Duration::parse) ?: Duration.of(60, ChronoUnit.SECONDS)
|
|
val maxRequestSize = child.renderAttribute("max-request-size")
|
|
?.let(Integer::decode) ?: 0x4000000
|
|
val chunkSize = child.renderAttribute("chunk-size")
|
|
?.let(Integer::decode) ?: 0x10000
|
|
connection = Configuration.Connection(
|
|
idleTimeout,
|
|
readIdleTimeout,
|
|
writeIdleTimeout,
|
|
maxRequestSize,
|
|
chunkSize
|
|
)
|
|
}
|
|
|
|
"event-executor" -> {
|
|
val useVirtualThread = child.renderAttribute("use-virtual-threads")
|
|
?.let(String::toBoolean) ?: true
|
|
eventExecutor = Configuration.EventExecutor(useVirtualThread)
|
|
}
|
|
|
|
"rate-limiter" -> {
|
|
val delayResponse = child.renderAttribute("delay-response")
|
|
?.let(String::toBoolean)
|
|
?: false
|
|
val messageBufferSize = child.renderAttribute("message-buffer-size")
|
|
?.let(Integer::decode)
|
|
?: 0x100000
|
|
val maxQueuedMessages = child.renderAttribute("max-queued-messages")
|
|
?.let(Integer::decode)
|
|
?: 100
|
|
rateLimiter = Configuration.RateLimiter(delayResponse, messageBufferSize, maxQueuedMessages)
|
|
}
|
|
|
|
"tls" -> {
|
|
var keyStore: KeyStore? = null
|
|
var trustStore: TrustStore? = null
|
|
|
|
for (granChild in child.asIterable()) {
|
|
when (granChild.localName) {
|
|
"keystore" -> {
|
|
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,
|
|
keyAlias,
|
|
keyPassword
|
|
)
|
|
}
|
|
|
|
"truststore" -> {
|
|
val trustStoreFile = Paths.get(granChild.renderAttribute("file"))
|
|
val trustStorePassword = granChild.renderAttribute("password")
|
|
val checkCertificateStatus = granChild.renderAttribute("check-certificate-status")
|
|
?.let(String::toBoolean)
|
|
?: false
|
|
val requireClientCertificate = child.renderAttribute("require-client-certificate")
|
|
?.let(String::toBoolean) ?: false
|
|
|
|
trustStore = TrustStore(
|
|
trustStoreFile,
|
|
trustStorePassword,
|
|
checkCertificateStatus,
|
|
requireClientCertificate
|
|
)
|
|
}
|
|
}
|
|
}
|
|
tls = Tls(keyStore, trustStore)
|
|
}
|
|
}
|
|
}
|
|
return Configuration.of(
|
|
host,
|
|
port,
|
|
proxyProtocolEnabled,
|
|
trustedProxies,
|
|
incomingConnectionsBacklogSize,
|
|
serverPath,
|
|
eventExecutor,
|
|
rateLimiter,
|
|
connection,
|
|
users,
|
|
groups,
|
|
cache!!,
|
|
authentication,
|
|
tls,
|
|
)
|
|
}
|
|
|
|
private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
|
|
when (it.localName) {
|
|
"reader" -> Role.Reader
|
|
"writer" -> Role.Writer
|
|
"healthcheck" -> Role.Healthcheck
|
|
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
|
|
}
|
|
}.toSet()
|
|
|
|
private fun parseTrustedProxies(root: Element) = root.asIterable().asSequence().map {
|
|
when (it.localName) {
|
|
"allow" -> it.renderAttribute("cidr")
|
|
?.let(Cidr::from)
|
|
?: throw ConfigurationException("Missing 'cidr' attribute")
|
|
else -> throw ConfigurationException("Unrecognized tag '${it.localName}'")
|
|
}
|
|
}.toList()
|
|
|
|
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map {
|
|
when (it.localName) {
|
|
"user" -> it.renderAttribute("ref")
|
|
"anonymous" -> ""
|
|
else -> ConfigurationException("Unrecognized tag '${it.localName}'")
|
|
}
|
|
}
|
|
|
|
private fun parseQuota(el: Element): Configuration.Quota {
|
|
val calls = el.renderAttribute("calls")
|
|
?.let(String::toLong)
|
|
?: throw ConfigurationException("Missing attribute 'calls'")
|
|
val maxAvailableCalls = el.renderAttribute("max-available-calls")
|
|
?.let(String::toLong)
|
|
?: calls
|
|
val initialAvailableCalls = el.renderAttribute("initial-available-calls")
|
|
?.let(String::toLong)
|
|
?: maxAvailableCalls
|
|
val period = el.renderAttribute("period")
|
|
?.let(Duration::parse)
|
|
?: throw ConfigurationException("Missing attribute 'period'")
|
|
return Configuration.Quota(calls, period, initialAvailableCalls, maxAvailableCalls)
|
|
}
|
|
|
|
private fun parseUsers(root: Element): Sequence<User> {
|
|
return root.asIterable().asSequence().mapNotNull { child ->
|
|
when (child.localName) {
|
|
"user" -> {
|
|
val username = child.renderAttribute("name")
|
|
val password = child.renderAttribute("password")
|
|
var quota: Configuration.Quota? = null
|
|
for (gchild in child.asIterable()) {
|
|
if (gchild.localName == "quota") {
|
|
quota = parseQuota(gchild)
|
|
}
|
|
}
|
|
User(username, password, emptySet(), quota)
|
|
}
|
|
"anonymous" -> {
|
|
var quota: Configuration.Quota? = null
|
|
for (gchild in child.asIterable()) {
|
|
if (gchild.localName == "quota") {
|
|
quota= parseQuota(gchild)
|
|
}
|
|
}
|
|
User("", null, emptySet(), quota)
|
|
}
|
|
else -> null
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun parseGroups(root: Element, knownUsers: Sequence<User>): Pair<Map<String, User>, Map<String, Group>> {
|
|
val knownUsersMap = knownUsers.associateBy(User::getName)
|
|
val userGroups = mutableMapOf<String, MutableSet<String>>()
|
|
val groups = root.asIterable().asSequence().filter {
|
|
it.localName == "group"
|
|
}.map { el ->
|
|
val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required")
|
|
var roles = emptySet<Role>()
|
|
var userQuota: Configuration.Quota? = null
|
|
var groupQuota: Configuration.Quota? = null
|
|
for (child in el.asIterable()) {
|
|
when (child.localName) {
|
|
"users" -> {
|
|
parseUserRefs(child).mapNotNull(knownUsersMap::get).forEach { user ->
|
|
userGroups.computeIfAbsent(user.name) {
|
|
mutableSetOf()
|
|
}.add(groupName)
|
|
}
|
|
}
|
|
|
|
"roles" -> {
|
|
roles = parseRoles(child)
|
|
}
|
|
"group-quota" -> {
|
|
userQuota = parseQuota(child)
|
|
}
|
|
"user-quota" -> {
|
|
groupQuota = parseQuota(child)
|
|
}
|
|
}
|
|
}
|
|
groupName to Group(groupName, roles, userQuota, groupQuota)
|
|
}.toMap()
|
|
val users = knownUsersMap.map { (name, user) ->
|
|
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet(), user.quota)
|
|
}.toMap()
|
|
return users to groups
|
|
}
|
|
} |