temporary commit

This commit is contained in:
2024-12-17 10:03:13 +08:00
parent f28ecca45e
commit 13f7ecc88a
23 changed files with 669 additions and 190 deletions

View File

@@ -1,19 +1,33 @@
plugins { plugins {
id 'application'
alias catalog.plugins.kotlin.jvm alias catalog.plugins.kotlin.jvm
alias catalog.plugins.graalvm.native.image
alias catalog.plugins.graalvm.jlink
alias catalog.plugins.envelope alias catalog.plugins.envelope
id 'maven-publish' id 'maven-publish'
} }
import net.woggioni.gradle.envelope.EnvelopeJarTask import net.woggioni.gradle.envelope.EnvelopeJarTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = 'net.woggioni' group = 'net.woggioni'
version = getProperty('gbcs.version') version = getProperty('gbcs.version')
application {
mainModule = 'net.woggioni.gbcs'
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer'
// mainClass = 'net.woggioni.gbcs.NettyPingServer'
}
configureNativeImage {
mainClass = 'net.woggioni.gbcs.GraalNativeImageConfiguration'
}
repositories { repositories {
maven { maven {
url = 'https://woggioni.net/mvn' url = getProperty('gitea.maven.url')
content { content {
includeModule 'net.woggioni', 'jwo' includeModule 'net.woggioni', 'jwo'
includeGroup 'com.lys' includeGroup 'com.lys'
@@ -27,7 +41,11 @@ dependencies {
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.netty.codec.http implementation catalog.netty.codec.http
runtimeOnly catalog.slf4j.jdk14 // runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic
// // https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
// runtimeOnly group: 'org.fusesource.jansi', name: 'jansi', version: '2.4.1'
testImplementation catalog.junit.jupiter.api testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params testImplementation catalog.junit.jupiter.params
@@ -36,39 +54,37 @@ dependencies {
java { java {
withSourcesJar() withSourcesJar()
modularity.inferModulePath = true
toolchain {
languageVersion = JavaLanguageVersion.of(21)
// vendor = JvmVendorSpec.GRAAL_VM
}
}
test {
useJUnitPlatform()
} }
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
modularity.inferModulePath = true
options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs=' + project.sourceSets.main.output.asPath options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs=' + project.sourceSets.main.output.asPath
options.release = 17
} }
tasks.withType(JavaCompile) {
modularity.inferModulePath = true
options.release = 21
}
tasks.named("compileKotlin", KotlinCompile.class) { tasks.named("compileKotlin", KotlinCompile.class) {
kotlinOptions { compilerOptions.jvmTarget = JvmTarget.JVM_21
jvmTarget = 17
}
} }
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) { Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
mainModule = 'net.woggioni.gbcs' // systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer' // systemProperties['log.config.source'] = 'logging.properties'
systemProperty 'java.util.logging.config.class', 'net.woggioni.gbcs.LoggingConfig' systemProperties['logback.configurationFile'] = 'classpath:logback.xml'
systemProperty 'log.config.source', 'logging.properties'
manifest {
attributes([
'Add-Exports' : 'java.base/sun.security.x509'
])
}
} }
envelopeRun {
mainModule = 'net.woggioni.envelope'
modularity.inferModulePath = true
jvmArgs('--add-exports=java.base/sun.security.x509=io.netty.handler')
}
def envelopeJarArtifact = artifacts.add('archives', envelopeJarTaskProvider.get().archiveFile.get().asFile) { def envelopeJarArtifact = artifacts.add('archives', envelopeJarTaskProvider.get().archiveFile.get().asFile) {
type = 'jar' type = 'jar'

View File

@@ -1,3 +1,5 @@
gbcs.version = 0.1-SNAPSHOT gbcs.version = 2024.12.13
lys.version = 0.2-SNAPSHOT lys.version = 2024.12.07
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven

Binary file not shown.

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

35
gradlew vendored
View File

@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,11 @@ do
esac esac
done done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # This is normally unused
# shellcheck disable=SC2034
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -133,22 +131,29 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
case $MAX_FD in #( case $MAX_FD in #(
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

21
gradlew.bat vendored
View File

@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -42,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@@ -0,0 +1 @@
Args=-H:Optimize=3 -H:+TraceClassInitialization

View File

@@ -1,23 +1,17 @@
pluginManagement { pluginManagement {
repositories { repositories {
mavenLocal()
maven { maven {
content { url = getProperty('gitea.maven.url')
includeModule 'net.woggioni.gradle', 'envelope'
includeModule 'net.woggioni.gradle.envelope', 'net.woggioni.gradle.envelope.gradle.plugin'
includeModule 'net.woggioni.gradle', 'lombok'
includeModule 'net.woggioni.gradle.lombok', 'net.woggioni.gradle.lombok.gradle.plugin'
}
url = 'https://woggioni.net/mvn/'
} }
gradlePluginPortal() gradlePluginPortal()
} }
includeBuild('../envelope')
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
maven { maven {
url = 'https://woggioni.net/mvn/' url = getProperty('gitea.maven.url')
content { content {
includeGroup 'com.lys' includeGroup 'com.lys'
} }
@@ -26,6 +20,7 @@ dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
catalog { catalog {
from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version') from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version')
version('envelope', '2024.12.15')
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import java.net.URLStreamHandlerFactory; import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider;
module net.woggioni.gbcs { module net.woggioni.gbcs {
requires java.sql;
requires java.xml; requires java.xml;
requires java.logging; requires java.logging;
requires kotlin.stdlib; requires kotlin.stdlib;
@@ -14,6 +15,8 @@ module net.woggioni.gbcs {
requires net.woggioni.jwo; requires net.woggioni.jwo;
exports net.woggioni.gbcs; exports net.woggioni.gbcs;
opens net.woggioni.gbcs to net.woggioni.envelope; exports net.woggioni.gbcs.url;
// opens net.woggioni.gbcs to net.woggioni.envelope;
provides java.net.URLStreamHandlerFactory with ClasspathUrlStreamHandlerFactoryProvider;
uses java.net.URLStreamHandlerFactory; uses java.net.URLStreamHandlerFactory;
} }

View File

@@ -0,0 +1,88 @@
package net.woggioni.gbcs;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyPingServer {
private final int port;
public NettyPingServer(int port) {
this.port = port;
}
public void start() throws Exception {
// Create event loop groups for handling incoming connections and processing
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// Create server bootstrap configuration
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new StringDecoder(CharsetUtil.UTF_8),
new StringEncoder(CharsetUtil.UTF_8),
new PingServerHandler()
);
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Bind and start the server
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("Ping Server started on port: " + port);
try(final var handle = new GradleBuildCacheServer.ServerHandle(future, bossGroup, workerGroup)) {
Thread.sleep(5000);
future.channel().close();
// Wait until the server socket is closed
future.channel().closeFuture().sync();
}
} finally {
// Shutdown event loop groups
// workerGroup.shutdownGracefully();
// bossGroup.shutdownGracefully();
}
}
// Custom handler for processing ping requests
private static class PingServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// Check if the received message is a ping request
if ("ping".equalsIgnoreCase(msg.trim())) {
// Respond with "pong"
ctx.writeAndFlush("pong\n");
System.out.println("Received ping, sent pong");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Log and close the connection in case of any errors
cause.printStackTrace();
ctx.close();
}
}
// Main method to start the server
public static void main(String[] args) throws Exception {
int port = 8080; // Default port
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
new NettyPingServer(port).start();
}
}

View File

@@ -0,0 +1,41 @@
package net.woggioni.gbcs.url;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
public class ClasspathUrlStreamHandlerFactoryProvider implements URLStreamHandlerFactory {
private static class Handler extends URLStreamHandler {
private final ClassLoader classLoader;
public Handler() {
this.classLoader = getClass().getClassLoader();
}
public Handler(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
protected URLConnection openConnection(URL u) throws IOException {
final URL resourceUrl = classLoader.getResource(u.getPath());
return resourceUrl.openConnection();
}
}
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
URLStreamHandler result;
switch (protocol) {
case "classpath":
result = new Handler();
break;
default:
result = null;
}
return result;
}
}

View File

@@ -16,7 +16,9 @@ import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class ClientCertificateValidator private constructor(private val sslHandler : SslHandler, private val x509TrustManager: X509TrustManager) : ChannelInboundHandlerAdapter() { class ClientCertificateValidator private constructor(
private val sslHandler : SslHandler,
private val x509TrustManager: X509TrustManager) : ChannelInboundHandlerAdapter() {
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
if (evt is SslHandshakeCompletionEvent) { if (evt is SslHandshakeCompletionEvent) {
if (evt.isSuccess) { if (evt.isSuccess) {
@@ -32,20 +34,17 @@ class ClientCertificateValidator private constructor(private val sslHandler : Ss
} }
companion object { companion object {
fun getTrustManager(trustStore : KeyStore?, certificateRevocationEnabled : Boolean) : X509TrustManager {
fun of(sslHandler : SslHandler, trustStore : KeyStore?) : ClientCertificateValidator { return if(trustStore != null) {
val certificateFactory = CertificateFactory.getInstance("X.509") val certificateFactory = CertificateFactory.getInstance("X.509")
val validator = CertPathValidator.getInstance("PKIX").apply {
val validator = CertPathValidator.getInstance("PKIX").apply { val rc = revocationChecker as PKIXRevocationChecker
val rc = revocationChecker as PKIXRevocationChecker rc.options = EnumSet.of(
rc.options = EnumSet.of( PKIXRevocationChecker.Option.NO_FALLBACK)
PKIXRevocationChecker.Option.NO_FALLBACK, }
PKIXRevocationChecker.Option.SOFT_FAIL, val params = PKIXParameters(trustStore).apply {
PKIXRevocationChecker.Option.PREFER_CRLS) isRevocationEnabled = certificateRevocationEnabled
} }
val manager = if(trustStore != null) {
val params = PKIXParameters(trustStore)
object : X509TrustManager { object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) { override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList()) val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
@@ -56,15 +55,23 @@ class ClientCertificateValidator private constructor(private val sslHandler : Ss
throw NotImplementedError() throw NotImplementedError()
} }
override fun getAcceptedIssuers(): Array<X509Certificate> { private val acceptedIssuers = trustStore.aliases().asSequence()
throw NotImplementedError() .filter (trustStore::isCertificateEntry)
} .map(trustStore::getCertificate)
.map { it as X509Certificate }
.toList()
.toTypedArray()
override fun getAcceptedIssuers() = acceptedIssuers
} }
} else { } else {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }.single() as X509TrustManager trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }.single() as X509TrustManager
} }
return ClientCertificateValidator(sslHandler, manager) }
fun of(sslHandler : SslHandler, trustStore : KeyStore?, certificateRevocationEnabled : Boolean) : ClientCertificateValidator {
return ClientCertificateValidator(sslHandler, getTrustManager(trustStore, certificateRevocationEnabled))
} }
} }
} }

View File

@@ -21,6 +21,7 @@ data class KeyStore(
data class TrustStore( data class TrustStore(
val file : Path, val file : Path,
val password : String?, val password : String?,
val checkCertificateStatus : Boolean
) )
data class Configuration( data class Configuration(
@@ -28,18 +29,26 @@ data class Configuration(
val host : String, val host : String,
val port : Int, val port : Int,
val users : Map<String, Set<Role>>, val users : Map<String, Set<Role>>,
val groups : Map<String, Set<Role>>,
val tlsConfiguration: TlsConfiguration?, val tlsConfiguration: TlsConfiguration?,
val serverPath : String val serverPath : String,
val useVirtualThread : Boolean
) { ) {
companion object { companion object {
fun parse(document : Element) : Configuration { fun parse(document : Element) : Configuration {
var cacheFolder = Paths.get(System.getProperty("user.home")).resolve(".gbcs") var cacheFolder = Paths.get(System.getProperty("user.home")).resolve(".gbcs")
var host : String = "127.0.0.1" var host = "127.0.0.1"
var port : Int = 11080 var port = 11080
var users = emptyMap<String, Set<Role>>() val users = emptyMap<String, Set<Role>>()
val groups = emptyMap<String, Set<Role>>()
var tlsConfiguration : TlsConfiguration? = null var tlsConfiguration : TlsConfiguration? = null
var serverPath = "/" val serverPath = document.getAttribute("path")
.takeIf(String::isNotEmpty)
?: "/"
val useVirtualThread = document.getAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
for(child in document.asIterable()) { for(child in document.asIterable()) {
when(child.nodeName) { when(child.nodeName) {
@@ -62,8 +71,8 @@ data class Configuration(
val trustStoreFile = Paths.get(granChild.getAttribute("file")) val trustStoreFile = Paths.get(granChild.getAttribute("file"))
val trustStorePassword = granChild.getAttribute("password") val trustStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty) .takeIf(String::isNotEmpty)
val keyAlias = granChild.getAttribute("server-key-alias") val keyAlias = granChild.getAttribute("key-alias")
val keyPasswordPassword = granChild.getAttribute("server-key-password") val keyPasswordPassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty) .takeIf(String::isNotEmpty)
keyStore = KeyStore( keyStore = KeyStore(
trustStoreFile, trustStoreFile,
@@ -76,9 +85,14 @@ data class Configuration(
val trustStoreFile = Paths.get(granChild.getAttribute("file")) val trustStoreFile = Paths.get(granChild.getAttribute("file"))
val trustStorePassword = granChild.getAttribute("password") val trustStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty) .takeIf(String::isNotEmpty)
val checkCertificateStatus = granChild.getAttribute("check-certificate-status")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: false
trustStore = TrustStore( trustStore = TrustStore(
trustStoreFile, trustStoreFile,
trustStorePassword trustStorePassword,
checkCertificateStatus
) )
} }
} }
@@ -89,7 +103,7 @@ data class Configuration(
} }
return Configuration(cacheFolder, host, port, users, tlsConfiguration, serverPath) return Configuration(cacheFolder, host, port, users, groups, tlsConfiguration, serverPath, useVirtualThread)
} }
} }
} }

View File

@@ -13,13 +13,15 @@ import java.security.MessageDigest
import java.security.PrivateKey import java.security.PrivateKey
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.AbstractMap.SimpleEntry import java.util.AbstractMap.SimpleEntry
import java.util.Arrays
import java.util.Base64 import java.util.Base64
import java.util.ServiceLoader import java.util.concurrent.Executors
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.Channel import io.netty.channel.Channel
import io.netty.channel.ChannelDuplexHandler import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelFuture
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer import io.netty.channel.ChannelInitializer
@@ -30,11 +32,13 @@ import io.netty.channel.EventLoopGroup
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioServerSocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.codec.DecoderException
import io.netty.handler.codec.compression.CompressionOptions import io.netty.handler.codec.compression.CompressionOptions
import io.netty.handler.codec.http.DefaultFullHttpResponse import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.DefaultHttpContent import io.netty.handler.codec.http.DefaultHttpContent
import io.netty.handler.codec.http.DefaultHttpResponse import io.netty.handler.codec.http.DefaultHttpResponse
import io.netty.handler.codec.http.FullHttpRequest import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpContentCompressor import io.netty.handler.codec.http.HttpContentCompressor
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues import io.netty.handler.codec.http.HttpHeaderValues
@@ -44,16 +48,23 @@ import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpServerCodec import io.netty.handler.codec.http.HttpServerCodec
import io.netty.handler.codec.http.HttpUtil import io.netty.handler.codec.http.HttpUtil
import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.codec.http.LastHttpContent import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.util.SelfSignedCertificate
import io.netty.handler.stream.ChunkedNioFile import io.netty.handler.stream.ChunkedNioFile
import io.netty.handler.stream.ChunkedWriteHandler import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.DefaultEventExecutorGroup import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.DefaultThreadFactory
import io.netty.util.concurrent.EventExecutorGroup import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
import javax.net.ssl.SSLPeerUnverifiedException
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2 import net.woggioni.jwo.Tuple2
import java.net.URI
import java.util.concurrent.ForkJoinPool
class GradleBuildCacheServer(private val cfg : Configuration) { class GradleBuildCacheServer(private val cfg : Configuration) {
@@ -125,6 +136,33 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
private class ServerInitializer(private val cfg : Configuration) : ChannelInitializer<Channel>() { private class ServerInitializer(private val cfg : Configuration) : ChannelInitializer<Channel>() {
private fun createSslCtx(tlsConfiguration : TlsConfiguration) : SslContext {
val keyStore = tlsConfiguration.keyStore
return if(keyStore == null) {
throw IllegalArgumentException("No keystore configured")
} else {
val javaKeyStore = loadKeystore(keyStore.file, keyStore.password)
val serverKey = javaKeyStore.getKey(
keyStore.keyAlias, keyStore.keyPassword?.let(String::toCharArray)) as PrivateKey
val serverCert : Array<X509Certificate> = Arrays.stream(javaKeyStore.getCertificateChain(keyStore.keyAlias))
.map { it as X509Certificate }
.toArray {size -> Array<X509Certificate?>(size) { null } }
SslContextBuilder.forServer(serverKey, *serverCert).apply {
if(tlsConfiguration.verifyClients) {
clientAuth(ClientAuth.REQUIRE)
val trustStore = tlsConfiguration.trustStore
if (trustStore != null) {
val ts = loadKeystore(trustStore.file, trustStore.password)
trustManager(
ClientCertificateValidator.getTrustManager(ts, trustStore.checkCertificateStatus))
}
}
}.build()
}
}
private val sslContext : SslContext? = cfg.tlsConfiguration?.let(this::createSslCtx)
companion object { companion object {
val group: EventExecutorGroup = DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors()) val group: EventExecutorGroup = DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors())
fun loadKeystore(file : Path, password : String?) : KeyStore { fun loadKeystore(file : Path, password : String?) : KeyStore {
@@ -132,13 +170,13 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
.map(Tuple2<String, String>::get_2) .map(Tuple2<String, String>::get_2)
.orElseThrow { .orElseThrow {
IllegalArgumentException( IllegalArgumentException(
"Keystore file '${file}' must have .jks or p12 extension") "Keystore file '${file}' must have .jks, .p12, .pfx extension")
} }
val keystore = when(ext.lowercase()) { val keystore = when(ext.substring(1).lowercase()) {
"jks" -> KeyStore.getInstance("JKS") "jks" -> KeyStore.getInstance("JKS")
"p12", "pfx" -> KeyStore.getInstance("PKCS12") "p12", "pfx" -> KeyStore.getInstance("PKCS12")
else -> throw IllegalArgumentException( else -> throw IllegalArgumentException(
"Keystore file '${file}' must have .jks or p12 extension") "Keystore file '${file}' must have .jks, .p12, .pfx extension")
} }
Files.newInputStream(file).use { Files.newInputStream(file).use {
keystore.load(it, password?.let(String::toCharArray)) keystore.load(it, password?.let(String::toCharArray))
@@ -148,34 +186,17 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
} }
override fun initChannel(ch: Channel) { override fun initChannel(ch: Channel) {
val userAuthorizer = UserAuthorizer(cfg.users)
val pipeline = ch.pipeline() val pipeline = ch.pipeline()
val tlsConfiguration = cfg.tlsConfiguration if(sslContext != null) {
if(tlsConfiguration != null) { val sslHandler = sslContext.newHandler(ch.alloc())
val ssc = SelfSignedCertificate()
val keyStore = tlsConfiguration.keyStore
val sslCtx = if(keyStore == null) {
SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build()
} else {
val javaKeyStore = loadKeystore(keyStore.file, keyStore.password)
val serverKey = javaKeyStore.getKey(
keyStore.keyAlias, keyStore.keyPassword?.let(String::toCharArray)) as PrivateKey
val serverCert = javaKeyStore.getCertificateChain(keyStore.keyAlias) as Array<X509Certificate>
SslContextBuilder.forServer(serverKey, *serverCert).build()
}
val sslHandler = sslCtx.newHandler(ch.alloc())
pipeline.addLast(sslHandler) pipeline.addLast(sslHandler)
if(tlsConfiguration.verifyClients) {
val trustStore = tlsConfiguration.trustStore?.let {
loadKeystore(it.file, it.password)
}
pipeline.addLast(ClientCertificateValidator.of(sslHandler, trustStore))
}
} }
pipeline.addLast(HttpServerCodec()) pipeline.addLast(HttpServerCodec())
pipeline.addLast(HttpChunkContentCompressor(1024)) pipeline.addLast(HttpChunkContentCompressor(1024))
pipeline.addLast(ChunkedWriteHandler()) pipeline.addLast(ChunkedWriteHandler())
pipeline.addLast(HttpObjectAggregator(Int.MAX_VALUE)) pipeline.addLast(HttpObjectAggregator(Int.MAX_VALUE))
// pipeline.addLast(NettyHttpBasicAuthenticator(mapOf("user" to "password")) { user, _ -> user == "user" }) // pipeline.addLast(NettyHttpBasicAuthenticator(mapOf("user" to "password")), userAuthorizer)
pipeline.addLast(group, ServerHandler(cfg.cacheFolder, cfg.serverPath)) pipeline.addLast(group, ServerHandler(cfg.cacheFolder, cfg.serverPath))
pipeline.addLast(ExceptionHandler()) pipeline.addLast(ExceptionHandler())
Files.createDirectories(cfg.cacheFolder) Files.createDirectories(cfg.cacheFolder)
@@ -184,9 +205,25 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
private class ExceptionHandler : ChannelDuplexHandler() { private class ExceptionHandler : ChannelDuplexHandler() {
private val log = contextLogger() private val log = contextLogger()
private val NOT_AUTHORIZED: FullHttpResponse = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER).apply {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
log.error(cause.message, cause) when(cause) {
ctx.close() is DecoderException -> {
log.error(cause.message, cause)
ctx.close()
}
is SSLPeerUnverifiedException -> {
ctx.writeAndFlush(NOT_AUTHORIZED.retainedDuplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
}
else -> {
log.error(cause.message, cause)
ctx.close()
}
}
} }
} }
@@ -288,35 +325,62 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
} }
} }
fun run() { class ServerHandle(
// Create the multithreaded event loops for the server private val httpChannel : ChannelFuture,
val bossGroup: EventLoopGroup = NioEventLoopGroup() private val bossGroup: EventLoopGroup,
val workerGroup: EventLoopGroup = NioEventLoopGroup() private val workerGroup: EventLoopGroup) : AutoCloseable {
try {
// A helper class that simplifies server configuration
val httpBootstrap = ServerBootstrap()
// Configure the server private val closeFuture : ChannelFuture = httpChannel.channel().closeFuture()
httpBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel::class.java)
.childHandler(ServerInitializer(cfg))
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
// Bind and start to accept incoming connections. fun shutdown() : ChannelFuture {
val bindAddress = InetSocketAddress(cfg.host, cfg.port) return httpChannel.channel().close()
val httpChannel = httpBootstrap.bind(bindAddress).sync()
// Wait until server socket is closed
httpChannel.channel().closeFuture().sync()
} finally {
workerGroup.shutdownGracefully()
bossGroup.shutdownGracefully()
} }
override fun close() {
try {
closeFuture.sync()
} finally {
val fut1 = workerGroup.shutdownGracefully()
val fut2 = if(bossGroup !== workerGroup) {
bossGroup.shutdownGracefully()
} else null
fut1.sync()
fut2?.sync()
}
}
}
fun run() : ServerHandle {
// Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup()
val serverSocketChannel = NioServerSocketChannel::class.java
val workerGroup = if(cfg.useVirtualThread) {
NioEventLoopGroup(0, Executors.newVirtualThreadPerTaskExecutor())
} else {
NioEventLoopGroup(0, Executors.newWorkStealingPool())
}
// A helper class that simplifies server configuration
val bootstrap = ServerBootstrap().apply {
// Configure the server
group(bossGroup, workerGroup)
channel(serverSocketChannel)
childHandler(ServerInitializer(cfg))
option(ChannelOption.SO_BACKLOG, 128)
childOption(ChannelOption.SO_KEEPALIVE, true)
}
// Bind and start to accept incoming connections.
val bindAddress = InetSocketAddress(cfg.host, cfg.port)
val httpChannel = bootstrap.bind(bindAddress).sync()
return ServerHandle(httpChannel, bossGroup, workerGroup)
} }
companion object { companion object {
private val log by lazy {
contextLogger()
}
private const val PROTOCOL_HANDLER = "java.protocol.handler.pkgs" private const val PROTOCOL_HANDLER = "java.protocol.handler.pkgs"
private const val HANDLERS_PACKAGE = "net.woggioni.gbcs.url" private const val HANDLERS_PACKAGE = "net.woggioni.gbcs.url"
@@ -341,13 +405,9 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
resetCachedUrlHandlers() resetCachedUrlHandlers()
} }
@JvmStatic fun loadConfiguration(args: Array<String>) : Configuration {
fun main(args: Array<String>) { registerUrlProtocolHandler()
SelfSignedCertificate() URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
ServiceLoader.load(javaClass.module.layer, URLStreamHandlerFactory::class.java).stream().forEach {
println(it.type())
}
// registerUrlProtocolHandler()
Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader
val app = Application.builder("gbcs") val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR") .configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
@@ -355,30 +415,39 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
.build() .build()
val confDir = app.computeConfigurationDirectory() val confDir = app.computeConfigurationDirectory()
val configurationFile = confDir.resolve("gbcs.xml") val configurationFile = confDir.resolve("gbcs.xml")
log.info { "here" }
if(!Files.exists(configurationFile)) { if(!Files.exists(configurationFile)) {
Files.createDirectories(confDir) Files.createDirectories(confDir)
val defaultConfigurationFileResourcePath = "net/woggioni/gbcs/gbcs-default.xml" val defaultConfigurationFileResourcePath = "classpath:net/woggioni/gbcs/gbcs-default.xml"
val defaultConfigurationFileResource = GradleBuildCacheServer.javaClass.classLoader val defaultConfigurationFileResource = URI(defaultConfigurationFileResourcePath).toURL()
.getResource(defaultConfigurationFileResourcePath)
?: throw IllegalStateException(
"Missing default configuration file 'classpath:$defaultConfigurationFileResourcePath'")
Files.newOutputStream(configurationFile).use { outputStream -> Files.newOutputStream(configurationFile).use { outputStream ->
defaultConfigurationFileResource.openStream().use { inputStream -> defaultConfigurationFileResource.openStream().use { inputStream ->
JWO.copy(inputStream, outputStream) JWO.copy(inputStream, outputStream)
} }
} }
} }
val schemaResource = "net/woggioni/gbcs/gbcs.xsd" // val schemaUrl = javaClass.getResource("/net/woggioni/gbcs/gbcs.xsd")
val schemaUrl = URL("classpath:net/woggioni/gbcs/gbcs.xsd") val schemaUrl = URL.of(URI("classpath:net/woggioni/gbcs/gbcs.xsd"), null)
// val schemaUrl = GradleBuildCacheServer::class.java.classLoader.getResource(schemaResource)
// ?: throw IllegalStateException("Missing configuration schema '$schemaResource'")
val schemaUrl2 = URL(schemaUrl.toString())
val dbf = Xml.newDocumentBuilderFactory() val dbf = Xml.newDocumentBuilderFactory()
// dbf.schema = Xml.getSchema(this::class.java.module.getResourceAsStream("/net/woggioni/gbcs/gbcs.xsd"))
dbf.schema = Xml.getSchema(schemaUrl) dbf.schema = Xml.getSchema(schemaUrl)
val doc = Files.newInputStream(configurationFile) val db = dbf.newDocumentBuilder().apply {
.use(dbf.newDocumentBuilder()::parse) setErrorHandler(Xml.ErrorHandler(schemaUrl))
GradleBuildCacheServer(Configuration.parse(doc.documentElement)).run() }
val doc = Files.newInputStream(configurationFile).use(db::parse)
return Configuration.parse(doc.documentElement)
}
@JvmStatic
fun main(args: Array<String>) {
val configuration = loadConfiguration(args)
// Runtime.getRuntime().addShutdownHook(Thread {
// Thread.sleep(5000)
// javaClass.classLoader.loadClass("net.woggioni.jwo.exception.ChildProcessException")
// }
// )
GradleBuildCacheServer(configuration).run().use {
}
} }
fun digest(data : ByteArray, fun digest(data : ByteArray,
@@ -392,4 +461,14 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
return JWO.bytesToHex(digest(data, md)) return JWO.bytesToHex(digest(data, md))
} }
} }
}
object GraalNativeImageConfiguration {
@JvmStatic
fun main(args: Array<String>) {
val conf = GradleBuildCacheServer.loadConfiguration(args)
val handle = GradleBuildCacheServer(conf).run()
Thread.sleep(10_000)
handle.shutdown()
}
} }

View File

@@ -8,9 +8,6 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.logging.LogManager import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java) inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) { inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {

View File

@@ -0,0 +1,22 @@
package net.woggioni.gbcs
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest
class UserAuthorizer(private val users: Map<String, Set<Role>>) : Authorizer {
companion object {
private val METHOD_MAP = mapOf(
Role.Reader to setOf(HttpMethod.GET, HttpMethod.HEAD),
Role.Writer to setOf(HttpMethod.PUT, HttpMethod.POST)
)
}
override fun authorize(user: String, request: HttpRequest) = users[user]?.let { roles ->
val allowedMethods = roles.asSequence()
.mapNotNull(METHOD_MAP::get)
.flatten()
.toSet()
request.method() in allowedMethods
} ?: false
}

View File

@@ -1,5 +1,7 @@
package net.woggioni.gbcs package net.woggioni.gbcs
import java.io.InputStream
import java.io.InputStreamReader
import java.net.URL import java.net.URL
import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
@@ -7,14 +9,17 @@ import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING
import javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI import javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI
import javax.xml.parsers.DocumentBuilder import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.Source
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.Schema import javax.xml.validation.Schema
import javax.xml.validation.SchemaFactory import javax.xml.validation.SchemaFactory
import net.woggioni.jwo.xml.Xml
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.NodeList import org.w3c.dom.NodeList
import org.xml.sax.ErrorHandler import org.xml.sax.ErrorHandler as ErrHandler
import org.xml.sax.SAXNotRecognizedException import org.xml.sax.SAXNotRecognizedException
import org.xml.sax.SAXNotSupportedException import org.xml.sax.SAXNotSupportedException
import org.xml.sax.SAXParseException import org.xml.sax.SAXParseException
@@ -66,10 +71,10 @@ class ElementIterator(parent: Element, name: String? = null) : Iterator<Element>
object Xml { object Xml {
private class XmlErrorHandler(private val fileURL: URL) : ErrorHandler { class ErrorHandler(private val fileURL: URL) : ErrHandler {
companion object { companion object {
private val log = LoggerFactory.getLogger(XmlErrorHandler::class.java) private val log = LoggerFactory.getLogger(ErrorHandler::class.java)
} }
override fun warning(ex: SAXParseException) { override fun warning(ex: SAXParseException) {
@@ -118,9 +123,18 @@ object Xml {
sf.setFeature(FEATURE_SECURE_PROCESSING, true) sf.setFeature(FEATURE_SECURE_PROCESSING, true)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA) // disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD) // disableProperty(sf, ACCESS_EXTERNAL_DTD)
sf.errorHandler = ErrorHandler(schema)
return sf.newSchema(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(): DocumentBuilderFactory { fun newDocumentBuilderFactory(): DocumentBuilderFactory {
val dbf = DocumentBuilderFactory.newInstance() val dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature(FEATURE_SECURE_PROCESSING, true) dbf.setFeature(FEATURE_SECURE_PROCESSING, true)
@@ -129,6 +143,7 @@ object Xml {
dbf.isExpandEntityReferences = false dbf.isExpandEntityReferences = false
dbf.isIgnoringComments = true dbf.isIgnoringComments = true
dbf.isNamespaceAware = true dbf.isNamespaceAware = true
dbf.isValidating = false
return dbf return dbf
} }

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration>
<import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
<import class="ch.qos.logback.core.ConsoleAppender"/>
<appender name="console" class="ConsoleAppender">
<target>System.out</target>
<encoder class="PatternLayoutEncoder">
<pattern>%d [%highlight(%-5level)] \(%thread\) %logger{36} -%kvp- %msg %n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="console"/>
</root>
</configuration>

View File

@@ -2,8 +2,8 @@
handlers = java.util.logging.ConsoleHandler handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = FINEST java.util.logging.ConsoleHandler.level = FINER
java.util.logging.ConsoleHandler.filter = java.util.logging.ConsoleHandler.filter =
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format = %1$tF %1$tT [%4$s] %2$s %5$s %n java.util.logging.SimpleFormatter.format = %1$tF %1$tT [%4$s] %2$s %5$s %6$s%n
java.util.logging.ConsoleHandler.encoding = java.util.logging.ConsoleHandler.encoding =

View File

@@ -1,14 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<server xmlns="urn:gradle-build-cache-server" path="/cache"> <gbcs:server xmlns:gbcs="urn:net.woggioni.gbcs" path="/cache" useVirtualThreads="false">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache path="/tmp/gbcs"/> <cache path="/tmp/gbcs"/>
<groups> <authorization>
<readers> <users>
<!-- <user name="reader"/>--> <user name="user1" password="password"/>
</readers> <user name="user2" password="password"/>
<writers> <user name="user3" password="password"/>
<!-- <user name="writer"/>--> <user name="user4" password="password"/>
</writers> <user name="user5" password="password">
</groups> <roles>
<tls verify-clients="true"/> <reader/>
</server> <writer/>
</roles>
</user>
</users>
<groups>
<group name="group1">
<users>
<user ref="user1"/>
</users>
<roles>
<reader/>
<writer/>
</roles>
</group>
<group name="group2">
<users>
<user ref="user1"/>
<user ref="user2"/>
</users>
<roles>
<reader/>
<writer/>
</roles>
</group>
</groups>
</authorization>
<tls-certificate-authorization>
<group-extractor attribute-name="CN" pattern="(.*)"/>
<user-extractor attribute-name="CN" pattern="(.*)"/>
</tls-certificate-authorization>
<!-- <tls verify-clients="false">-->
<!-- <keystore file="" key-alias=""/>-->
<!-- <truststore file=""/>-->
<!-- </tls>-->
</gbcs:server>

View File

@@ -1,17 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema elementFormDefault="qualified" targetNamespace="urn:gradle-build-cache-server" version="1.0" <xs:schema targetNamespace="urn:net.woggioni.gbcs"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:gbcs="urn:gradle-build-cache-server"> xmlns:gbcs="urn:net.woggioni.gbcs">
<xs:element name="server" type="gbcs:serverType"/> <xs:element name="server" type="gbcs:serverType"/>
<xs:complexType name="serverType"> <xs:complexType name="serverType">
<xs:sequence minOccurs="0"> <xs:sequence minOccurs="0">
<xs:element name="bind" type="gbcs:bindType"/> <xs:element name="bind" type="gbcs:bindType" maxOccurs="1"/>
<xs:element name="cache" type="gbcs:cacheDirType"/> <xs:element name="cache" type="gbcs:cacheDirType" maxOccurs="1"/>
<xs:element name="groups" type="gbcs:groupsType"/> <xs:element name="authorization" type="gbcs:authorizationType" maxOccurs="1">
<xs:element name="tls" type="gbcs:tlsType"/> <xs:key name="userId">
<xs:selector xpath="users/user"/>
<xs:field xpath="@name"/>
</xs:key>
<xs:keyref name="userRef" refer="gbcs:userId">
<xs:selector xpath="groups/group/users/user"/>
<xs:field xpath="@ref"/>
</xs:keyref>
</xs:element>
<xs:element name="tls-certificate-authorization" type="gbcs:tlsCertificateAuthorizationType" minOccurs="0" maxOccurs="1"/>
<xs:element name="tls" type="gbcs:tlsType" minOccurs="0" maxOccurs="1"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="path" type="xs:string"/> <xs:attribute name="path" type="xs:string"/>
<xs:attribute name="useVirtualThreads" type="xs:boolean" use="optional"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="bindType"> <xs:complexType name="bindType">
@@ -23,26 +34,92 @@
<xs:attribute name="path" type="xs:string" use="required"/> <xs:attribute name="path" type="xs:string" use="required"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="groupsType"> <xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence>
<xs:element name="group-extractor" type="gbcs:X500NameExtractorType"/>
<xs:element name="user-extractor" type="gbcs:X500NameExtractorType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="X500NameExtractorType">
<xs:attribute name="attribute-name" type="xs:string"/>
<xs:attribute name="pattern" type="xs:string"/>
</xs:complexType>
<xs:complexType name="authorizationType">
<xs:all> <xs:all>
<xs:element name="readers" type="gbcs:groupType"/> <xs:element name="users" type="gbcs:usersType"/>
<xs:element name="writers" type="gbcs:groupType"/> <xs:element name="groups" type="gbcs:groupsType">
<xs:unique name="groupKey">
<xs:selector xpath="group"/>
<xs:field xpath="@name"/>
</xs:unique>
</xs:element>
</xs:all> </xs:all>
</xs:complexType> </xs:complexType>
<xs:complexType name="groupType"> <xs:complexType name="usersType">
<xs:sequence> <xs:sequence>
<xs:element maxOccurs="unbounded" minOccurs="0" name="user" type="gbcs:userType"/> <xs:element name="user" type="gbcs:userType" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:complexType name="userType"> <xs:complexType name="userType">
<xs:all minOccurs="0" maxOccurs="1">
<xs:element name="roles" type="gbcs:rolesType"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required"/> <xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="groupsType">
<xs:sequence>
<xs:element name="group" type="gbcs:groupType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="groupType">
<xs:sequence>
<xs:element name="users" type="gbcs:userRefsType" maxOccurs="1" minOccurs="0">
<xs:unique name="userRefWriterKey">
<xs:selector xpath="user"/>
<xs:field xpath="@ref"/>
</xs:unique>
</xs:element>
<xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string"/>
</xs:complexType>
<xs:simpleType name="role" final="restriction" >
<xs:restriction base="xs:string">
<xs:enumeration value="READER" />
<xs:enumeration value="WRITER" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="rolesType">
<xs:sequence>
<xs:choice maxOccurs="unbounded">
<xs:element name="writer"/>
<xs:element name="reader"/>
</xs:choice>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefsType">
<xs:sequence>
<xs:element name="user" type="gbcs:userRefType" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="userRefType">
<xs:attribute name="ref" type="xs:string" use="required"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="tlsType"> <xs:complexType name="tlsType">
<xs:all> <xs:all>
<xs:element name="keystore" type="gbcs:keyStoreType" minOccurs="0"/> <xs:element name="keystore" type="gbcs:keyStoreType" />
<xs:element name="truststore" type="gbcs:trustStoreType" minOccurs="0"/> <xs:element name="truststore" type="gbcs:trustStoreType" minOccurs="0"/>
</xs:all> </xs:all>
<xs:attribute name="verify-clients" type="xs:boolean" use="optional"/> <xs:attribute name="verify-clients" type="xs:boolean" use="optional"/>
@@ -58,6 +135,7 @@
<xs:complexType name="trustStoreType"> <xs:complexType name="trustStoreType">
<xs:attribute name="file" type="xs:string" use="required"/> <xs:attribute name="file" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string"/> <xs:attribute name="password" type="xs:string"/>
<xs:attribute name="check-certificate-status" type="xs:boolean"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="propertiesType"> <xs:complexType name="propertiesType">

View File

@@ -0,0 +1,35 @@
package net.woggioni.gbcs
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.cert.CertPathValidator
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import org.junit.jupiter.api.Test
class CertificateValidationTest {
@Test
fun test() {
val keystore = KeyStore.getInstance("PKCS12")
val keystorePath = Path.of("/home/woggioni/ssl/woggioni@f6aa5663ef26.pfx")
Files.newInputStream(keystorePath).use {
keystore.load(it, System.getenv("KEYPASS").toCharArray())
}
val pkix = CertPathValidator.getInstance("PKIX")
val trustStore = KeyStore.getInstance("PKCS12")
val trustStorePath = Path.of("/home/woggioni/ssl/truststore.pfx")
Files.newInputStream(trustStorePath).use {
trustStore.load(it, "123456".toCharArray())
}
val certificateFactory = CertificateFactory.getInstance("X.509")
val cert = keystore.getCertificateChain("woggioni@f6aa5663ef26").toList()
.let(certificateFactory::generateCertPath)
val params = PKIXParameters(trustStore)
params.isRevocationEnabled = false
pkix.validate(cert, params)
}
}

View File

@@ -0,0 +1,22 @@
package net.woggioni.gbcs
import java.net.URI
import java.net.URL
import org.junit.jupiter.api.Test
class ConfigurationTest {
@Test
fun test() {
GradleBuildCacheServer.registerUrlProtocolHandler()
val schemaUrl = this::class.java.getResource("/net/woggioni/gbcs/gbcs.xsd")
val dbf = Xml.newDocumentBuilderFactory()
dbf.schema = Xml.getSchema(schemaUrl)
val db = dbf.newDocumentBuilder().apply {
setErrorHandler(Xml.ErrorHandler(schemaUrl))
}
val configurationUrl = this::class.java.getResource("/net/woggioni/gbcs/gbcs-default.xml")
val doc = configurationUrl.openStream().use(db::parse)
Configuration.parse(doc.documentElement)
}
}