renamed project to "Remote Cache Build Server" (RBCS)

This commit is contained in:
2025-02-06 14:48:35 +08:00
parent 5fef1b932e
commit 6c0eadb9fb
118 changed files with 561 additions and 565 deletions

21
rbcs-common/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
plugins {
id 'java-library'
id 'maven-publish'
alias catalog.plugins.kotlin.jvm
}
dependencies {
implementation project(':rbcs-api')
implementation catalog.slf4j.api
implementation catalog.jwo
implementation catalog.netty.buffer
}
publishing {
publications {
maven(MavenPublication) {
from(components["java"])
}
}
}

View File

@@ -0,0 +1,11 @@
module net.woggioni.rbcs.common {
requires java.xml;
requires java.logging;
requires org.slf4j;
requires kotlin.stdlib;
requires net.woggioni.jwo;
requires io.netty.buffer;
provides java.net.spi.URLStreamHandlerProvider with net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory;
exports net.woggioni.rbcs.common;
}

View File

@@ -0,0 +1,25 @@
package net.woggioni.rbcs.common
import io.netty.buffer.ByteBuf
import java.io.InputStream
class ByteBufInputStream(private val buf : ByteBuf) : InputStream() {
override fun read(): Int {
return buf.takeIf {
it.readableBytes() > 0
}?.let(ByteBuf::readByte)
?.let(Byte::toInt) ?: -1
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
val readableBytes = buf.readableBytes()
if(readableBytes == 0) return -1
val result = len.coerceAtMost(readableBytes)
buf.readBytes(b, off, result)
return result
}
override fun close() {
buf.release()
}
}

View File

@@ -0,0 +1,18 @@
package net.woggioni.rbcs.common
import io.netty.buffer.ByteBuf
import java.io.OutputStream
class ByteBufOutputStream(private val buf : ByteBuf) : OutputStream() {
override fun write(b: Int) {
buf.writeByte(b)
}
override fun write(b: ByteArray, off: Int, len: Int) {
buf.writeBytes(b, off, len)
}
override fun close() {
buf.release()
}
}

View File

@@ -0,0 +1,7 @@
package net.woggioni.rbcs.common
class ResourceNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
}
class ModuleNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
}

View File

@@ -0,0 +1,8 @@
package net.woggioni.rbcs.common
data class HostAndPort(val host: String, val port: Int = 0) {
override fun toString(): String {
return "$host:$port"
}
}

View File

@@ -0,0 +1,111 @@
package net.woggioni.rbcs.common
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
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.log(level : Level, messageBuilder : () -> String) {
if(isEnabledForLevel(level)) {
makeLoggingEventBuilder(level).log(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())
}
}
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

@@ -0,0 +1,46 @@
package net.woggioni.rbcs.common
import java.security.SecureRandom
import java.security.spec.KeySpec
import java.util.Base64
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
object PasswordSecurity {
private const val KEY_LENGTH = 256
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
val result = ByteArray(arr1.size + arr2.size)
var j = 0
for(element in arr1) {
result[j] = element
j += 1
}
for(element in arr2) {
result[j] = element
j += 1
}
return result
}
fun hashPassword(password : String, salt : String? = null) : String {
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
val result = ByteArray(16)
nextBytes(result)
result
}
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val hash = factory.generateSecret(spec).encoded
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
}
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(passwordHash)
val hash = ByteArray(KEY_LENGTH / 8)
val salt = ByteArray(decoded.size - KEY_LENGTH / 8)
System.arraycopy(decoded, 0, hash, 0, hash.size)
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
return hash to salt
}
}

View File

@@ -0,0 +1,29 @@
package net.woggioni.rbcs.common
import net.woggioni.jwo.JWO
import java.net.URI
import java.net.URL
import java.security.MessageDigest
object RBCS {
fun String.toUrl() : URL = URL.of(URI(this), null)
const val RBCS_NAMESPACE_URI: String = "urn:net.woggioni.rbcs.server"
const val RBCS_PREFIX: String = "rbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
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,113 @@
package net.woggioni.rbcs.common
import java.io.IOException
import java.io.InputStream
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.net.spi.URLStreamHandlerProvider
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.Collectors
class RbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
private class ClasspathHandler(private val classLoader: ClassLoader = RbcsUrlStreamHandlerFactory::class.java.classLoader) :
URLStreamHandler() {
override fun openConnection(u: URL): URLConnection? {
return javaClass.module
?.takeIf { m: Module -> m.layer != null }
?.let {
val path = u.path
val i = path.lastIndexOf('/')
val packageName = path.substring(0, i).replace('/', '.')
val modules = packageMap[packageName]!!
ClasspathResourceURLConnection(
u,
modules
)
}
?: classLoader.getResource(u.path)?.let(URL::openConnection)
}
}
private class JpmsHandler : URLStreamHandler() {
override fun openConnection(u: URL): URLConnection {
val moduleName = u.host
val thisModule = javaClass.module
val sourceModule =
thisModule
?.let(Module::getLayer)
?.let { layer: ModuleLayer ->
layer.findModule(moduleName).orElse(null)
} ?: if(thisModule.layer == null) {
thisModule
} else throw ModuleNotFoundException("Module '$moduleName' not found")
return JpmsResourceURLConnection(u, sourceModule)
}
}
private class JpmsResourceURLConnection(url: URL, private val module: Module) : URLConnection(url) {
override fun connect() {
}
@Throws(IOException::class)
override fun getInputStream(): InputStream {
val resource = getURL().path
return module.getResourceAsStream(resource)
?: throw ResourceNotFoundException("Resource '$resource' not found in module '${module.name}'")
}
}
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
return when (protocol) {
"classpath" -> ClasspathHandler()
"jpms" -> JpmsHandler()
else -> null
}
}
private class ClasspathResourceURLConnection(url: URL?, private val modules: List<Module>) :
URLConnection(url) {
override fun connect() {}
override fun getInputStream(): InputStream? {
for (module in modules) {
val result = module.getResourceAsStream(getURL().path)
if (result != null) return result
}
return null
}
}
companion object {
private val installed = AtomicBoolean(false)
fun install() {
if (!installed.getAndSet(true)) {
URL.setURLStreamHandlerFactory(RbcsUrlStreamHandlerFactory())
}
}
private val packageMap: Map<String, List<Module>> by lazy {
RbcsUrlStreamHandlerFactory::class.java.module.layer
.modules()
.stream()
.flatMap { m: Module ->
m.packages.stream()
.map { p: String -> p to m }
}
.collect(
Collectors.groupingBy(
Pair<String, Module>::first,
Collectors.mapping(
Pair<String, Module>::second,
Collectors.toUnmodifiableList<Module>()
)
)
)
}
}
}

View File

@@ -0,0 +1,244 @@
package net.woggioni.rbcs.common
import net.woggioni.jwo.JWO
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
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(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)= err(ex, Level.WARN)
private fun err(ex: SAXParseException, level: Level) {
log.log(level) {
"Problem at ${fileURL}:${ex.lineNumber}:${ex.columnNumber} parsing deployment configuration: ${ex.message}"
}
throw ex
}
override fun error(ex: SAXParseException) = err(ex, Level.ERROR)
override fun fatalError(ex: SAXParseException) = err(ex, Level.ERROR)
}
companion object {
private val dictMap: Map<String, Map<String, Any>> = 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).replace("$$", "$")
}
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) }
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, false)
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)
return sf.newSchema(StreamSource(inputStream))
}
fun newDocumentBuilderFactory(schemaResourceURL: URL?): DocumentBuilderFactory {
val dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature(FEATURE_SECURE_PROCESSING, false)
dbf.setAttribute(ACCESS_EXTERNAL_SCHEMA, "all")
disableProperty(dbf, ACCESS_EXTERNAL_DTD)
dbf.isExpandEntityReferences = true
dbf.isIgnoringComments = true
dbf.isNamespaceAware = true
dbf.isValidating = schemaResourceURL == null
dbf.setFeature("http://apache.org/xml/features/validation/schema", true)
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 write(doc: Document, output: OutputStream) {
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
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,
namespaceURI: String? = null,
attrs: Map<String, String> = emptyMap(),
cb: Xml.(el: Element) -> Unit = {}
): Element {
val child = doc.createElementNS(namespaceURI, name)
for ((key, value) in attrs) {
child.setAttribute(key, value)
}
return child
.also {
element.appendChild(it)
Xml(doc, it).cb(it)
}
}
fun attr(key: String, value: String, namespaceURI: String? = null) {
element.setAttributeNS(namespaceURI, key, value)
}
fun text(txt: String) {
element.appendChild(doc.createTextNode(txt))
}
}

View File

@@ -0,0 +1 @@
net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory