temporary commit

This commit is contained in:
2025-01-08 23:17:43 +08:00
parent 688a196a52
commit 0fdb37fb54
74 changed files with 3302 additions and 675 deletions

View File

@@ -11,6 +11,7 @@ import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import io.netty.util.ReferenceCountUtil
import net.woggioni.gbcs.api.Role
import java.security.SecureRandom
import java.security.spec.KeySpec
import java.util.Base64

View File

@@ -1,6 +1,7 @@
package net.woggioni.gbcs
import io.netty.handler.codec.http.HttpRequest
import net.woggioni.gbcs.api.Role
fun interface Authorizer {
fun authorize(roles : Set<Role>, request: HttpRequest) : Boolean

View File

@@ -42,22 +42,30 @@ import io.netty.handler.stream.ChunkedNioStream
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.gbcs.cache.Cache
import net.woggioni.gbcs.cache.FileSystemCache
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.api.exception.ContentTooLargeException
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.GBCS.toUrl
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.configuration.Parser
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2
import java.io.ByteArrayOutputStream
import java.net.InetSocketAddress
import java.net.URI
import java.net.URL
import java.net.URLStreamHandlerFactory
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.Arrays
@@ -68,6 +76,7 @@ import java.util.regex.Pattern
import javax.naming.ldap.LdapName
import javax.net.ssl.SSLEngine
import javax.net.ssl.SSLPeerUnverifiedException
import kotlin.io.path.absolute
class GradleBuildCacheServer(private val cfg: Configuration) {
@@ -107,7 +116,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): Set<Role>? {
return try {
sslEngine.session.peerCertificates
} catch (es : SSLPeerUnverifiedException) {
} catch (es: SSLPeerUnverifiedException) {
null
}?.takeIf {
it.isNotEmpty()
@@ -187,13 +196,13 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
.map { it as X509Certificate }
.toArray { size -> Array<X509Certificate?>(size) { null } }
SslContextBuilder.forServer(serverKey, *serverCert).apply {
if (tls.verifyClients) {
if (tls.isVerifyClients) {
clientAuth(ClientAuth.OPTIONAL)
val trustStore = tls.trustStore
if (trustStore != null) {
val ts = loadKeystore(trustStore.file, trustStore.password)
trustManager(
ClientCertificateValidator.getTrustManager(ts, trustStore.checkCertificateStatus)
ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus)
)
}
}
@@ -259,7 +268,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
override fun initChannel(ch: Channel) {
val pipeline = ch.pipeline()
val auth = cfg.authentication
var authenticator : AbstractNettyHttpAuthenticator? = null
var authenticator: AbstractNettyHttpAuthenticator? = null
if (auth is Configuration.BasicAuthentication) {
val roleAuthorizer = RoleAuthorizer()
authenticator = (NettyHttpBasicAuthenticator(cfg.users, roleAuthorizer))
@@ -268,7 +277,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val sslHandler = sslContext.newHandler(ch.alloc())
pipeline.addLast(sslHandler)
if(auth is Configuration.ClientCertificateAuthentication) {
if (auth is Configuration.ClientCertificateAuthentication) {
val roleAuthorizer = RoleAuthorizer()
authenticator = ClientCertificateAuthenticator(
roleAuthorizer,
@@ -282,16 +291,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
pipeline.addLast(HttpChunkContentCompressor(1024))
pipeline.addLast(ChunkedWriteHandler())
pipeline.addLast(HttpObjectAggregator(Int.MAX_VALUE))
authenticator?.let{
authenticator?.let {
pipeline.addLast(it)
}
val cacheImplementation = when(val cache = cfg.cache) {
is Configuration.FileSystemCache -> {
FileSystemCache(cache.root, cache.maxAge)
}
else -> throw NotImplementedError()
}
pipeline.addLast(group, ServerHandler(cacheImplementation, cfg.serverPath))
val cacheImplementation = cfg.cache.materialize()
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
pipeline.addLast(group, ServerHandler(cacheImplementation, prefix))
pipeline.addLast(ExceptionHandler())
}
}
@@ -305,6 +310,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
}
private val TOO_BIG: FullHttpResponse = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, Unpooled.EMPTY_BUFFER
).apply {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
when (cause) {
is DecoderException -> {
@@ -317,6 +328,11 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
}
is ContentTooLargeException -> {
ctx.writeAndFlush(TOO_BIG.retainedDuplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
}
else -> {
log.error(cause.message, cause)
ctx.close()
@@ -325,7 +341,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
}
}
private class ServerHandler(private val cache: Cache, private val serverPrefix: String?) :
private class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
SimpleChannelInboundHandler<FullHttpRequest>() {
companion object {
@@ -335,7 +351,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val uri = req.uri()
val i = uri.lastIndexOf('/')
if (i < 0) throw RuntimeException(String.format("Malformed request URI: '%s'", uri))
return uri.substring(0, i).takeIf(String::isNotEmpty) to uri.substring(i + 1)
return uri.substring(0, i) to uri.substring(i + 1)
}
}
@@ -343,9 +359,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val keepAlive: Boolean = HttpUtil.isKeepAlive(msg)
val method = msg.method()
if (method === HttpMethod.GET) {
val (prefix, key) = splitPath(msg)
// val (prefix, key) = splitPath(msg)
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
cache.get(digestString(key.toByteArray()))?.let { channel ->
cache.get(key)?.let { channel ->
log.debug(ctx) {
"Cache hit for key '$key'"
}
@@ -369,6 +388,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
.addListener(ChannelFutureListener.CLOSE)
}
}
else -> {
ctx.write(ChunkedNioStream(channel))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
@@ -391,13 +411,24 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
ctx.writeAndFlush(response)
}
} else if (method === HttpMethod.PUT) {
val (prefix, key) = splitPath(msg)
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
log.debug(ctx) {
"Added value for key '$key' to build cache"
}
val content = msg.content()
cache.put(digestString(key.toByteArray()), content)
val bodyBytes = msg.content().run {
if (isDirect) {
ByteArray(readableBytes()).also {
readBytes(it)
}
} else {
array()
}
}
cache.put(key, bodyBytes)
val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray())
@@ -453,7 +484,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
// Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup()
val serverSocketChannel = NioServerSocketChannel::class.java
val workerGroup = if (cfg.useVirtualThread) {
val workerGroup = if (cfg.isUseVirtualThread) {
NioEventLoopGroup(0, Executors.newVirtualThreadPerTaskExecutor())
} else {
NioEventLoopGroup(0, Executors.newWorkStealingPool())
@@ -477,8 +508,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
companion object {
private fun String.toUrl() : URL = URL.of(URI(this), null)
private val log by lazy {
contextLogger()
}
@@ -486,9 +515,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
private const val PROTOCOL_HANDLER = "java.protocol.handler.pkgs"
private const val HANDLERS_PACKAGE = "net.woggioni.gbcs.url"
val CONFIGURATION_SCHEMA_URL by lazy {
"classpath:net/woggioni/gbcs/gbcs.xsd".toUrl()
}
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
/**
@@ -515,7 +541,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
fun loadConfiguration(args: Array<String>): Configuration {
// registerUrlProtocolHandler()
URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
// Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader
val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
@@ -524,6 +549,9 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val confDir = app.computeConfigurationDirectory()
val configurationFile = confDir.resolve("gbcs.xml")
if (!Files.exists(configurationFile)) {
log.info {
"Creating default configuration file at '$configurationFile'"
}
Files.createDirectories(confDir)
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
Files.newOutputStream(configurationFile).use { outputStream ->
@@ -533,38 +561,29 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
}
}
// val schemaUrl = javaClass.getResource("/net/woggioni/gbcs/gbcs.xsd")
val schemaUrl = CONFIGURATION_SCHEMA_URL
val dbf = Xml.newDocumentBuilderFactory(schemaUrl)
// val schemaUrl = GBCS.CONFIGURATION_SCHEMA_URL
val dbf = Xml.newDocumentBuilderFactory(null)
// dbf.schema = Xml.getSchema(this::class.java.module.getResourceAsStream("/net/woggioni/gbcs/gbcs.xsd"))
dbf.schema = Xml.getSchema(schemaUrl)
val db = dbf.newDocumentBuilder().apply {
setErrorHandler(Xml.ErrorHandler(schemaUrl))
}
// dbf.schema = Xml.getSchema(schemaUrl)
val db = dbf.newDocumentBuilder()
val doc = Files.newInputStream(configurationFile).use(db::parse)
return Configuration.parse(doc)
return Parser.parse(doc)
}
@JvmStatic
fun main(args: Array<String>) {
ClasspathUrlStreamHandlerFactoryProvider.install()
val configuration = loadConfiguration(args)
log.debug {
ByteArrayOutputStream().also {
Xml.write(Serializer.serialize(configuration), it)
}.let {
"Server configuration:\n${String(it.toByteArray())}"
}
}
GradleBuildCacheServer(configuration).run().use {
}
}
fun digest(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): String {
return JWO.bytesToHex(digest(data, md))
}
}
}

View File

@@ -2,87 +2,7 @@ package net.woggioni.gbcs
import io.netty.channel.ChannelHandlerContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isTraceEnabled) {
val (format, params) = messageBuilder()
trace(format, params)
}
}
inline fun Logger.debugParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isDebugEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.infoParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isInfoEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.warnParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isWarnEnabled) {
val (format, params) = messageBuilder()
warn(format, params)
}
}
inline fun Logger.errorParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isErrorEnabled) {
val (format, params) = messageBuilder()
error(format, params)
}
}
inline fun log(log : Logger,
filter : Logger.() -> Boolean,
loggerMethod : Logger.(String) -> Unit, messageBuilder : () -> String) {
if(log.filter()) {
log.loggerMethod(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder : () -> String) {
if(isTraceEnabled) {
trace(messageBuilder())
}
}
inline fun Logger.debug(messageBuilder : () -> String) {
if(isDebugEnabled) {
debug(messageBuilder())
}
}
inline fun Logger.info(messageBuilder : () -> String) {
if(isInfoEnabled) {
info(messageBuilder())
}
}
inline fun Logger.warn(messageBuilder : () -> String) {
if(isWarnEnabled) {
warn(messageBuilder())
}
}
inline fun Logger.error(messageBuilder : () -> String) {
if(isErrorEnabled) {
error(messageBuilder())
}
}
inline fun Logger.trace(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isTraceEnabled }, { trace(it) } , messageBuilder)
@@ -107,25 +27,4 @@ inline fun log(log : Logger, ctx : ChannelHandlerContext,
val clientAddress = (ctx.channel().remoteAddress() as InetSocketAddress).address.hostAddress
log.loggerMethod(clientAddress + " - " + messageBuilder())
}
}
class LoggingConfig {
init {
val logManager = LogManager.getLogManager()
System.getProperty("log.config.source")?.let withSource@ { source ->
val urls = LoggingConfig::class.java.classLoader.getResources(source)
while(urls.hasMoreElements()) {
val url = urls.nextElement()
url.openStream().use { inputStream ->
logManager.readConfiguration(inputStream)
return@withSource
}
}
Path.of(source).takeIf(Files::exists)
?.let(Files::newInputStream)
?.use(logManager::readConfiguration)
}
}
}

View File

@@ -1,5 +0,0 @@
package net.woggioni.gbcs
enum class Role {
Reader, Writer
}

View File

@@ -2,6 +2,7 @@ package net.woggioni.gbcs
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest
import net.woggioni.gbcs.api.Role
class RoleAuthorizer : Authorizer {

View File

@@ -1,262 +0,0 @@
package net.woggioni.gbcs
import org.slf4j.LoggerFactory
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.SAXNotRecognizedException
import org.xml.sax.SAXNotSupportedException
import org.xml.sax.SAXParseException
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING
import javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.Schema
import javax.xml.validation.SchemaFactory
import org.xml.sax.ErrorHandler as ErrHandler
class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> {
private var cursor: Int = 0
override fun hasNext(): Boolean {
return cursor < nodeList.length
}
override fun next(): Node {
return if (hasNext()) nodeList.item(cursor++) else throw NoSuchElementException()
}
}
class ElementIterator(parent: Element, name: String? = null) : Iterator<Element> {
private val it: NodeListIterator
private val name: String?
private var next: Element?
init {
it = NodeListIterator(parent.childNodes)
this.name = name
next = getNext()
}
override fun hasNext(): Boolean {
return next != null
}
override fun next(): Element {
val result = next ?: throw NoSuchElementException()
next = getNext()
return result
}
private fun getNext(): Element? {
var result: Element? = null
while (it.hasNext()) {
val node: Node = it.next()
if (node is Element && (name == null || name == node.tagName)) {
result = node
break
}
}
return result
}
}
class Xml(private val doc: Document, val element: Element) {
class ErrorHandler(private val fileURL: URL) : ErrHandler {
companion object {
private val log = LoggerFactory.getLogger(ErrorHandler::class.java)
}
override fun warning(ex: SAXParseException) {
log.warn(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
}
override fun error(ex: SAXParseException) {
log.error(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
throw ex
}
override fun fatalError(ex: SAXParseException) {
log.error(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
throw ex
}
}
companion object {
fun Element.asIterable() = Iterable { ElementIterator(this, null) }
fun NodeList.asIterable() = Iterable { NodeListIterator(this) }
private fun disableProperty(dbf: DocumentBuilderFactory, propertyName: String) {
try {
dbf.setAttribute(propertyName, "")
} catch (iae: IllegalArgumentException) {
// Property not supported.
}
}
private fun disableProperty(sf: SchemaFactory, propertyName: String) {
try {
sf.setProperty(propertyName, "")
} catch (ex: SAXNotRecognizedException) {
// Property not supported.
} catch (ex: SAXNotSupportedException) {
}
}
fun getSchema(schema: URL): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, true)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD)
sf.errorHandler = ErrorHandler(schema)
return sf.newSchema(schema)
}
fun getSchema(inputStream: InputStream): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, true)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD)
return sf.newSchema(StreamSource(inputStream))
}
fun newDocumentBuilderFactory(schemaResourceURL: URL?): DocumentBuilderFactory {
val dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature(FEATURE_SECURE_PROCESSING, true)
disableProperty(dbf, ACCESS_EXTERNAL_SCHEMA)
disableProperty(dbf, ACCESS_EXTERNAL_DTD)
dbf.isExpandEntityReferences = false
dbf.isIgnoringComments = true
dbf.isNamespaceAware = true
dbf.isValidating = false
schemaResourceURL?.let {
dbf.schema = getSchema(it)
}
return dbf
}
fun newDocumentBuilder(resource: URL, schemaResourceURL: URL?): DocumentBuilder {
val db = newDocumentBuilderFactory(schemaResourceURL).newDocumentBuilder()
db.setErrorHandler(ErrorHandler(resource))
return db
}
fun parseXmlResource(resource: URL, schemaResourceURL: URL?): Document {
val db = newDocumentBuilder(resource, schemaResourceURL)
return resource.openStream().use(db::parse)
}
fun parseXml(sourceURL : URL, sourceStream: InputStream? = null, schemaResourceURL: URL? = null): Document {
val db = newDocumentBuilder(sourceURL, schemaResourceURL)
return sourceStream?.let(db::parse) ?: sourceURL.openStream().use(db::parse)
}
//
// fun newDocumentBuilder(resource: URL): DocumentBuilder {
// val db = newDocumentBuilderFactory(null).newDocumentBuilder()
// db.setErrorHandler(XmlErrorHandler(resource))
// return db
// }
// fun parseXmlResource(resource: URL): Document {
// val db = newDocumentBuilder(resource, null)
// return resource.openStream().use(db::parse)
// }
fun write(doc: Document, output: OutputStream) {
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
// val domImpl = doc.getImplementation()
// val docType = domImpl.createDocumentType(
// "plist",
// "-//Apple//DTD PLIST 1.0//EN",
// "http://www.apple.com/DTDs/PropertyList-1.0.dtd"
// )
// transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, docType.getPublicId())
// transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, docType.getSystemId())
// val transformerFactory = TransformerFactory.newInstance()
// val transformer: Transformer = transformerFactory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes")
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
val source = DOMSource(doc)
val result = StreamResult(output)
transformer.transform(source, result)
}
fun of(namespaceURI: String, qualifiedName: String, schemaResourceURL: URL? = null, cb: Xml.(el: Element) -> Unit): Document {
val dbf = newDocumentBuilderFactory(schemaResourceURL)
val db = dbf.newDocumentBuilder()
val doc = db.newDocument()
val root = doc.createElementNS(namespaceURI, qualifiedName)
.also(doc::appendChild)
Xml(doc, root).cb(root)
return doc
}
fun of(doc: Document, el: Element, cb: Xml.(el: Element) -> Unit): Element {
Xml(doc, el).cb(el)
return el
}
fun Element.removeChildren() {
while (true) {
removeChild(firstChild ?: break)
}
}
}
fun node(
name: String,
attrs: Map<String, String> = emptyMap(),
cb: Xml.(el: Element) -> Unit = {}
): Element {
val child = doc.createElement(name)
for ((key, value) in attrs) {
child.setAttribute(key, value)
}
return child
.also {
element.appendChild(it)
Xml(doc, it).cb(it)
}
}
fun attrs(vararg attributes: Pair<String, String>) {
for (attr in attributes) element.setAttribute(attr.first, attr.second)
}
fun attr(key: String, value: String) {
element.setAttribute(key, value)
}
fun text(txt: String) {
element.appendChild(doc.createTextNode(txt))
}
}

View File

@@ -1,10 +0,0 @@
package net.woggioni.gbcs.cache
import io.netty.buffer.ByteBuf
import java.nio.channels.ByteChannel
interface Cache {
fun get(key : String) : ByteChannel?
fun put(key : String, content : ByteBuf) : Unit
}

View File

@@ -1,20 +1,32 @@
package net.woggioni.gbcs.cache
import io.netty.buffer.ByteBuf
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.digestString
import net.woggioni.gbcs.api.Cache
import net.woggioni.jwo.JWO
import net.woggioni.jwo.LockFile
import java.nio.channels.Channels
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
class FileSystemCache(val root: Path, val maxAge: Duration) : Cache {
class FileSystemCache(
val root: Path,
val maxAge: Duration,
val digestAlgorithm: String?,
val compressionEnabled: Boolean,
val compressionLevel: Int
) : Cache {
private fun lockFilePath(key: String): Path = root.resolve("$key.lock")
@@ -22,40 +34,52 @@ class FileSystemCache(val root: Path, val maxAge: Duration) : Cache {
Files.createDirectories(root)
}
override fun equals(other: Any?): Boolean {
return when (other) {
is FileSystemCache -> {
other.root == root && other.maxAge == maxAge
}
private var nextGc = AtomicReference(Instant.now().plus(maxAge))
else -> false
override fun get(key: String) = (digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
LockFile.acquire(lockFilePath(digest), true).use {
root.resolve(digest).takeIf(Files::exists)?.let { file ->
if (compressionEnabled) {
val inflater = Inflater()
Channels.newChannel(InflaterInputStream(Files.newInputStream(file), inflater))
} else {
FileChannel.open(file, StandardOpenOption.READ)
}
}
}.also {
gc()
}
}
override fun hashCode(): Int {
return root.hashCode() xor maxAge.hashCode()
}
private var nextGc = AtomicReference(Instant.now().plus(maxAge))
override fun get(key: String) = LockFile.acquire(lockFilePath(key), true).use {
root.resolve(key).takeIf(Files::exists)?.let { FileChannel.open(it, StandardOpenOption.READ) }
}.also {
gc()
}
override fun put(key: String, content: ByteBuf) {
LockFile.acquire(lockFilePath(key), false).use {
val file = root.resolve(key)
val tmpFile = Files.createTempFile(root, null, ".tmp")
try {
Files.newOutputStream(tmpFile).use {
content.readBytes(it, content.readableBytes())
override fun put(key: String, content: ByteArray) {
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
LockFile.acquire(lockFilePath(digest), false).use {
val file = root.resolve(digest)
val tmpFile = Files.createTempFile(root, null, ".tmp")
try {
Files.newOutputStream(tmpFile).let {
if (compressionEnabled) {
val deflater = Deflater(compressionLevel)
DeflaterOutputStream(it, deflater)
} else {
it
}
}.use {
it.write(content)
}
Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE)
} catch (t: Throwable) {
Files.delete(tmpFile)
throw t
}
Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE)
} catch (t: Throwable) {
Files.delete(tmpFile)
throw t
}
}.also {
gc()
@@ -87,4 +111,23 @@ class FileSystemCache(val root: Path, val maxAge: Duration) : Cache {
Files.delete(lockFile)
}
}
override fun close() {}
companion object {
fun digest(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): String {
return JWO.bytesToHex(digest(data, md))
}
}
}

View File

@@ -0,0 +1,27 @@
package net.woggioni.gbcs.cache
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.GBCS
import net.woggioni.jwo.Application
import java.nio.file.Path
import java.time.Duration
data class FileSystemCacheConfiguration(
val root: Path?,
val maxAge: Duration,
val digestAlgorithm : String?,
val compressionEnabled: Boolean,
val compressionLevel: Int,
) : Configuration.Cache {
override fun materialize() = FileSystemCache(
root ?: Application.builder("gbcs").build().computeCacheDirectory(),
maxAge,
digestAlgorithm,
compressionEnabled,
compressionLevel
)
override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
override fun getTypeName() = "fileSystemCacheType"
}

View File

@@ -0,0 +1,66 @@
package net.woggioni.gbcs.cache
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.Xml
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.nio.file.Path
import java.time.Duration
import java.util.zip.Deflater
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/schema/gbcs.xsd"
override fun getXmlType() = "fileSystemCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.gbcs"
override fun deserialize(el: Element): FileSystemCacheConfiguration {
val path = el.getAttribute("path")
.takeIf(String::isNotEmpty)
?.let(Path::of)
val maxAge = el.getAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse)
?: Duration.ofDays(1)
val enableCompression = el.getAttribute("enable-compression")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: true
val compressionLevel = el.getAttribute("compression-level")
.takeIf(String::isNotEmpty)
?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) ?: "MD5"
return FileSystemCacheConfiguration(
path,
maxAge,
digestAlgorithm,
enableCompression,
compressionLevel
)
}
override fun serialize(doc: Document, cache : FileSystemCacheConfiguration) = cache.run {
val result = doc.createElement("cache")
Xml.of(doc, result) {
val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI)
attr("xs:type", "${prefix}:fileSystemCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI)
attr("path", root.toString())
attr("max-age", maxAge.toString())
digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm)
}
attr("enable-compression", compressionEnabled.toString())
compressionLevel.takeIf {
it != Deflater.DEFAULT_COMPRESSION
}?.let {
attr("compression-level", it.toString())
}
}
result
}
}

View File

@@ -0,0 +1,15 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.Configuration
import java.util.ServiceLoader
object CacheSerializers {
val index = (Configuration::class.java.module.layer?.let { layer ->
ServiceLoader.load(layer, CacheProvider::class.java)
} ?: ServiceLoader.load(CacheProvider::class.java))
.asSequence()
.map {
(it.xmlNamespace to it.xmlType) to it
}.toMap()
}

View File

@@ -1,14 +1,11 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.Role
import net.woggioni.gbcs.Xml.Companion.asIterable
import org.w3c.dom.Document
import org.w3c.dom.Element
import net.woggioni.gbcs.api.Role
import java.nio.file.Path
import java.nio.file.Paths
import java.security.cert.X509Certificate
import java.time.Duration
@ConsistentCopyVisibility
data class Configuration private constructor(
val host: String,
val port: Int,
@@ -36,10 +33,6 @@ data class Configuration private constructor(
get() = groups.asSequence().flatMap { it.roles }.toSet()
}
data class HostAndPort(val host: String, val port: Int) {
override fun toString() = "$host:$port"
}
fun interface UserExtractor {
fun extract(cert :X509Certificate) : User
}
@@ -107,188 +100,210 @@ data class Configuration private constructor(
useVirtualThread
)
fun parse(document: Document): Configuration {
val root = document.documentElement
var cache: Cache? = null
var host = "127.0.0.1"
var port = 11080
var users = emptyMap<String, User>()
var groups = emptyMap<String, Group>()
var tls: Tls? = null
val serverPath = root.getAttribute("path")
val useVirtualThread = root.getAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var authentication : Authentication? = null
for (child in root.asIterable()) {
when (child.nodeName) {
"authorization" -> {
for (gchild in child.asIterable()) {
when (child.nodeName) {
"users" -> {
users = parseUsers(child)
}
"groups" -> {
val pair = parseGroups(child, users)
users = pair.first
groups = pair.second
}
}
}
}
"bind" -> {
host = child.getAttribute("host")
port = Integer.parseInt(child.getAttribute("port"))
}
"cache" -> {
for (gchild in child.asIterable()) {
when (gchild.nodeName) {
"file-system-cache" -> {
val cacheFolder = gchild.getAttribute("path")
.takeIf(String::isNotEmpty)
?.let(Paths::get)
?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
val maxAge = gchild.getAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse)
?: Duration.ofDays(1)
cache = FileSystemCache(cacheFolder, maxAge)
}
}
}
}
"authentication" -> {
for (gchild in child.asIterable()) {
when (gchild.nodeName) {
"basic" -> {
authentication = BasicAuthentication()
}
"client-certificate" -> {
var tlsExtractorUser : TlsCertificateExtractor? = null
var tlsExtractorGroup : TlsCertificateExtractor? = null
for (gchild in child.asIterable()) {
when (gchild.nodeName) {
"group-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
}
"user-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
}
}
}
authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
}
}
}
}
"tls" -> {
val verifyClients = child.getAttribute("verify-clients")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var keyStore: KeyStore? = null
var trustStore: TrustStore? = null
for (granChild in child.asIterable()) {
when (granChild.nodeName) {
"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)
keyStore = KeyStore(
keyStoreFile,
keyStorePassword,
keyAlias,
keyPassword
)
}
"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)
?.let(String::toBoolean)
?: false
trustStore = TrustStore(
trustStoreFile,
trustStorePassword,
checkCertificateStatus
)
}
}
}
tls = Tls(keyStore, trustStore, verifyClients)
}
}
}
return of(host, port, serverPath, users, groups, cache!!, authentication, tls, useVirtualThread)
}
private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
when (it.nodeName) {
"reader" -> Role.Reader
"writer" -> Role.Writer
else -> throw UnsupportedOperationException("Illegal node '${it.nodeName}'")
}
}.toSet()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter {
it.nodeName == "user"
}.map {
it.getAttribute("ref")
}.toSet()
private fun parseUsers(root: Element): Map<String, User> {
return root.asIterable().asSequence().filter {
it.nodeName == "user"
}.map { el ->
val username = el.getAttribute("name")
val password = el.getAttribute("password").takeIf(String::isNotEmpty)
username to User(username, password, emptySet())
}.toMap()
}
private fun parseGroups(root: Element, knownUsers : Map<String, User>): Pair<Map<String, User>, Map<String, Group>> {
val userGroups = mutableMapOf<String, MutableSet<String>>()
val groups = root.asIterable().asSequence().filter {
it.nodeName == "group"
}.map { el ->
val groupName = el.getAttribute("name")
var roles = emptySet<Role>()
for (child in el.asIterable()) {
when (child.nodeName) {
"users" -> {
parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user ->
userGroups.computeIfAbsent(user.name) {
mutableSetOf()
}.add(groupName)
}
}
"roles" -> {
roles = parseRoles(child)
}
}
}
groupName to Group(groupName, roles)
}.toMap()
val users = knownUsers.map { (name, user) ->
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
}.toMap()
return users to groups
}
// fun parse(document: Document): Configuration {
// val cacheSerializers = ServiceLoader.load(Configuration::class.java.module.layer, CacheSerializer::class.java)
// .asSequence()
// .map {
// "${it.xmlType}:${it.xmlNamespace}" to it
// }.toMap()
// val root = document.documentElement
// var cache: Cache? = null
// var host = "127.0.0.1"
// var port = 11080
// var users = emptyMap<String, User>()
// var groups = emptyMap<String, Group>()
// var tls: Tls? = null
// val serverPath = root.getAttribute("path")
// val useVirtualThread = root.getAttribute("useVirtualThreads")
// .takeIf(String::isNotEmpty)
// ?.let(String::toBoolean) ?: false
// var authentication : Authentication? = null
// for (child in root.asIterable()) {
// when (child.nodeName) {
// "authorization" -> {
// for (gchild in child.asIterable()) {
// when (child.nodeName) {
// "users" -> {
// users = parseUsers(child)
// }
//
// "groups" -> {
// val pair = parseGroups(child, users)
// users = pair.first
// groups = pair.second
// }
// }
// }
// }
//
// "bind" -> {
// host = child.getAttribute("host")
// port = Integer.parseInt(child.getAttribute("port"))
// }
//
// "cache" -> {
// val type = child.getAttribute("xs:type")
// val serializer = cacheSerializers.get(type) ?: throw NotImplementedError()
// cache = serializer.deserialize(child)
//
// when(child.getAttribute("xs:type")) {
// "gbcs:fileSystemCacheType" -> {
// val cacheFolder = child.getAttribute("path")
// .takeIf(String::isNotEmpty)
// ?.let(Paths::get)
// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
// val maxAge = child.getAttribute("max-age")
// .takeIf(String::isNotEmpty)
// ?.let(Duration::parse)
// ?: Duration.ofDays(1)
// cache = FileSystemCache(cacheFolder, maxAge)
// }
// }
//// for (gchild in child.asIterable()) {
//// when (gchild.nodeName) {
//// "file-system-cache" -> {
//// val cacheFolder = gchild.getAttribute("path")
//// .takeIf(String::isNotEmpty)
//// ?.let(Paths::get)
//// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
//// val maxAge = gchild.getAttribute("max-age")
//// .takeIf(String::isNotEmpty)
//// ?.let(Duration::parse)
//// ?: Duration.ofDays(1)
//// cache = FileSystemCache(cacheFolder, maxAge)
//// }
//// }
//// }
// }
//
// "authentication" -> {
// for (gchild in child.asIterable()) {
// when (gchild.nodeName) {
// "basic" -> {
// authentication = BasicAuthentication()
// }
//
// "client-certificate" -> {
// var tlsExtractorUser : TlsCertificateExtractor? = null
// var tlsExtractorGroup : TlsCertificateExtractor? = null
// for (gchild in child.asIterable()) {
// when (gchild.nodeName) {
// "group-extractor" -> {
// val attrName = gchild.getAttribute("attribute-name")
// val pattern = gchild.getAttribute("pattern")
// tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
// }
//
// "user-extractor" -> {
// val attrName = gchild.getAttribute("attribute-name")
// val pattern = gchild.getAttribute("pattern")
// tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
// }
// }
// }
// authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
// }
// }
// }
// }
//
// "tls" -> {
// val verifyClients = child.getAttribute("verify-clients")
// .takeIf(String::isNotEmpty)
// ?.let(String::toBoolean) ?: false
// var keyStore: KeyStore? = null
// var trustStore: TrustStore? = null
// for (granChild in child.asIterable()) {
// when (granChild.nodeName) {
// "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)
// keyStore = KeyStore(
// keyStoreFile,
// keyStorePassword,
// keyAlias,
// keyPassword
// )
// }
//
// "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)
// ?.let(String::toBoolean)
// ?: false
// trustStore = TrustStore(
// trustStoreFile,
// trustStorePassword,
// checkCertificateStatus
// )
// }
// }
// }
// tls = Tls(keyStore, trustStore, verifyClients)
// }
// }
// }
// return of(host, port, serverPath, users, groups, cache!!, authentication, tls, useVirtualThread)
// }
//
// private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
// when (it.nodeName) {
// "reader" -> Role.Reader
// "writer" -> Role.Writer
// else -> throw UnsupportedOperationException("Illegal node '${it.nodeName}'")
// }
// }.toSet()
//
// private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter {
// it.nodeName == "user"
// }.map {
// it.getAttribute("ref")
// }.toSet()
//
// private fun parseUsers(root: Element): Map<String, User> {
// return root.asIterable().asSequence().filter {
// it.nodeName == "user"
// }.map { el ->
// val username = el.getAttribute("name")
// val password = el.getAttribute("password").takeIf(String::isNotEmpty)
// username to User(username, password, emptySet())
// }.toMap()
// }
//
// private fun parseGroups(root: Element, knownUsers : Map<String, User>): Pair<Map<String, User>, Map<String, Group>> {
// val userGroups = mutableMapOf<String, MutableSet<String>>()
// val groups = root.asIterable().asSequence().filter {
// it.nodeName == "group"
// }.map { el ->
// val groupName = el.getAttribute("name")
// var roles = emptySet<Role>()
// for (child in el.asIterable()) {
// when (child.nodeName) {
// "users" -> {
// parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user ->
// userGroups.computeIfAbsent(user.name) {
// mutableSetOf()
// }.add(groupName)
// }
// }
// "roles" -> {
// roles = parseRoles(child)
// }
// }
// }
// groupName to Group(groupName, roles)
// }.toMap()
// val users = knownUsers.map { (name, user) ->
// name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
// }.toMap()
// return users to groups
// }
}
}

View File

@@ -0,0 +1,231 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Configuration.Authentication
import net.woggioni.gbcs.api.Configuration.BasicAuthentication
import net.woggioni.gbcs.api.Configuration.Cache
import net.woggioni.gbcs.api.Configuration.ClientCertificateAuthentication
import net.woggioni.gbcs.api.Configuration.Group
import net.woggioni.gbcs.api.Configuration.KeyStore
import net.woggioni.gbcs.api.Configuration.Tls
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.base.Xml.Companion.asIterable
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.TypeInfo
import java.nio.file.Paths
object Parser {
fun parse(document: Document): Configuration {
val root = document.documentElement
var cache: Cache? = null
var host = "127.0.0.1"
var port = 11080
var users = emptyMap<String, User>()
var groups = emptyMap<String, Group>()
var tls: Tls? = null
val serverPath = root.getAttribute("path")
val useVirtualThread = root.getAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var authentication: Authentication? = null
for (child in root.asIterable()) {
when (child.localName) {
"authorization" -> {
for (gchild in child.asIterable()) {
when (child.localName) {
"users" -> {
users = parseUsers(child)
}
"groups" -> {
val pair = parseGroups(child, users)
users = pair.first
groups = pair.second
}
}
}
}
"bind" -> {
host = child.getAttribute("host")
port = Integer.parseInt(child.getAttribute("port"))
}
"cache" -> {
// val type = child.getAttribute("xs:type").split(":")
// val namespaceURI = child.lookupNamespaceURI(type[0])
// val typeName = type[1]
cache = (child as? TypeInfo)?.let { tf ->
val typeNamespace = tf.typeNamespace
val typeName = tf.typeName
CacheSerializers.index[typeNamespace to typeName]
}?.deserialize(child) ?: throw NotImplementedError()
// cache = serializer.deserialize(child)
// when(child.getAttribute("xs:type")) {
// "gbcs:fileSystemCacheType" -> {
// val cacheFolder = child.getAttribute("path")
// .takeIf(String::isNotEmpty)
// ?.let(Paths::get)
// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
// val maxAge = child.getAttribute("max-age")
// .takeIf(String::isNotEmpty)
// ?.let(Duration::parse)
// ?: Duration.ofDays(1)
// cache = FileSystemCache(cacheFolder, maxAge)
// }
// }
// for (gchild in child.asIterable()) {
// when (gchild.localName) {
// "file-system-cache" -> {
// val cacheFolder = gchild.getAttribute("path")
// .takeIf(String::isNotEmpty)
// ?.let(Paths::get)
// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
// val maxAge = gchild.getAttribute("max-age")
// .takeIf(String::isNotEmpty)
// ?.let(Duration::parse)
// ?: Duration.ofDays(1)
// cache = FileSystemCache(cacheFolder, maxAge)
// }
// }
// }
}
"authentication" -> {
for (gchild in child.asIterable()) {
when (gchild.localName) {
"basic" -> {
authentication = BasicAuthentication()
}
"client-certificate" -> {
var tlsExtractorUser: TlsCertificateExtractor? = null
var tlsExtractorGroup: TlsCertificateExtractor? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"group-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
}
"user-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
}
}
}
authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
}
}
}
}
"tls" -> {
val verifyClients = child.getAttribute("verify-clients")
.takeIf(String::isNotEmpty)
?.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)
keyStore = KeyStore(
keyStoreFile,
keyStorePassword,
keyAlias,
keyPassword
)
}
"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)
?.let(String::toBoolean)
?: false
trustStore = TrustStore(
trustStoreFile,
trustStorePassword,
checkCertificateStatus
)
}
}
}
tls = Tls(keyStore, trustStore, verifyClients)
}
}
}
return Configuration(host, port, serverPath, users, groups, cache!!, authentication, tls, useVirtualThread)
}
private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
when (it.localName) {
"reader" -> Role.Reader
"writer" -> Role.Writer
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
}
}.toSet()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter {
it.localName == "user"
}.map {
it.getAttribute("ref")
}.toSet()
private fun parseUsers(root: Element): Map<String, User> {
return root.asIterable().asSequence().filter {
it.localName == "user"
}.map { el ->
val username = el.getAttribute("name")
val password = el.getAttribute("password").takeIf(String::isNotEmpty)
username to User(username, password, emptySet())
}.toMap()
}
private fun parseGroups(root: Element, knownUsers: Map<String, User>): Pair<Map<String, User>, Map<String, Group>> {
val userGroups = mutableMapOf<String, MutableSet<String>>()
val groups = root.asIterable().asSequence().filter {
it.localName == "group"
}.map { el ->
val groupName = el.getAttribute("name")
var roles = emptySet<Role>()
for (child in el.asIterable()) {
when (child.localName) {
"users" -> {
parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user ->
userGroups.computeIfAbsent(user.name) {
mutableSetOf()
}.add(groupName)
}
}
"roles" -> {
roles = parseRoles(child)
}
}
}
groupName to Group(groupName, roles)
}.toMap()
val users = knownUsers.map { (name, user) ->
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
}.toMap()
return users to groups
}
}

View File

@@ -1,34 +1,37 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.Xml
import org.w3c.dom.Document
object Serializer {
private const val GBCS_NAMESPACE: String = "urn:net.woggioni.gbcs"
private const val GBCS_PREFIX: String = "gbcs"
fun serialize(conf : Configuration) : Document {
return Xml.of(GBCS_NAMESPACE, GBCS_PREFIX + ":server") {
attr("userVirtualThreads", conf.useVirtualThread.toString())
conf.serverPath?.let { serverPath ->
val schemaLocations = CacheSerializers.index.values.asSequence().map {
it.xmlNamespace to it.xmlSchemaLocation
}.toMap()
return Xml.of(GBCS.GBCS_NAMESPACE_URI, GBCS.GBCS_PREFIX + ":server") {
attr("useVirtualThreads", conf.isUseVirtualThread.toString())
// attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI)
val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ")
attr("xs:schemaLocation",value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI)
conf.serverPath
?.takeIf(String::isNotEmpty)
?.let { serverPath ->
attr("path", serverPath)
}
node("bind") {
attr("host", conf.host)
attr("port", conf.port.toString())
}
node("cache") {
when(val cache = conf.cache) {
is Configuration.FileSystemCache -> {
node("file-system-cache") {
attr("path", cache.root.toString())
attr("max-age", cache.maxAge.toString())
}
}
else -> throw NotImplementedError()
}
}
val cache = conf.cache
val serializer : CacheProvider<Configuration.Cache> =
(CacheSerializers.index[cache.namespaceURI to cache.typeName] as? CacheProvider<Configuration.Cache>) ?: throw NotImplementedError()
element.appendChild(serializer.serialize(doc, cache))
node("authorization") {
node("users") {
for(user in conf.users.values) {
@@ -112,7 +115,7 @@ object Serializer {
trustStore.password?.let { password ->
attr("password", password)
}
attr("check-certificate-status", trustStore.checkCertificateStatus.toString())
attr("check-certificate-status", trustStore.isCheckCertificateStatus.toString())
}
}
}