diff --git a/build.gradle b/build.gradle index 6d8c209..38506ba 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,33 @@ plugins { + id 'application' alias catalog.plugins.kotlin.jvm + alias catalog.plugins.graalvm.native.image + alias catalog.plugins.graalvm.jlink alias catalog.plugins.envelope id 'maven-publish' } import net.woggioni.gradle.envelope.EnvelopeJarTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = 'net.woggioni' 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 { maven { - url = 'https://woggioni.net/mvn' + url = getProperty('gitea.maven.url') content { includeModule 'net.woggioni', 'jwo' includeGroup 'com.lys' @@ -27,7 +41,11 @@ dependencies { implementation catalog.slf4j.api 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.params @@ -36,39 +54,37 @@ dependencies { java { withSourcesJar() + modularity.inferModulePath = true + toolchain { + languageVersion = JavaLanguageVersion.of(21) +// vendor = JvmVendorSpec.GRAAL_VM + } +} + +test { + useJUnitPlatform() } tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { - modularity.inferModulePath = true 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) { - kotlinOptions { - jvmTarget = 17 - } + compilerOptions.jvmTarget = JvmTarget.JVM_21 } Provider envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) { - mainModule = 'net.woggioni.gbcs' - mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer' - systemProperty 'java.util.logging.config.class', 'net.woggioni.gbcs.LoggingConfig' - systemProperty 'log.config.source', 'logging.properties' - - manifest { - attributes([ - 'Add-Exports' : 'java.base/sun.security.x509' - ]) - } +// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig' +// systemProperties['log.config.source'] = 'logging.properties' + systemProperties['logback.configurationFile'] = 'classpath:logback.xml' } -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) { type = 'jar' diff --git a/gradle.properties b/gradle.properties index 0d3a954..6d7ce5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..d64cd49 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 070cb70..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME 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 zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (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. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,11 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +131,29 @@ location of your Java installation." fi else 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 location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( 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 ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | 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" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# 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 -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index f127cfd..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -42,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/native-image/native-image.properties b/native-image/native-image.properties new file mode 100644 index 0000000..3a2e1e7 --- /dev/null +++ b/native-image/native-image.properties @@ -0,0 +1 @@ +Args=-H:Optimize=3 -H:+TraceClassInitialization \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e33858b..1fa5696 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,23 +1,17 @@ pluginManagement { repositories { + mavenLocal() maven { - content { - 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/' + url = getProperty('gitea.maven.url') } gradlePluginPortal() } - includeBuild('../envelope') } dependencyResolutionManagement { repositories { maven { - url = 'https://woggioni.net/mvn/' + url = getProperty('gitea.maven.url') content { includeGroup 'com.lys' } @@ -26,6 +20,7 @@ dependencyResolutionManagement { versionCatalogs { catalog { from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version') + version('envelope', '2024.12.15') } } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 680563f..f083c6a 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,7 @@ -import java.net.URLStreamHandlerFactory; +import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider; module net.woggioni.gbcs { + requires java.sql; requires java.xml; requires java.logging; requires kotlin.stdlib; @@ -14,6 +15,8 @@ module net.woggioni.gbcs { requires net.woggioni.jwo; 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; } \ No newline at end of file diff --git a/src/main/java/net/woggioni/gbcs/NettyPingServer.java b/src/main/java/net/woggioni/gbcs/NettyPingServer.java new file mode 100644 index 0000000..1a2c363 --- /dev/null +++ b/src/main/java/net/woggioni/gbcs/NettyPingServer.java @@ -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() { + @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 { + @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(); + } +} + diff --git a/src/main/java/net/woggioni/gbcs/url/ClasspathUrlStreamHandlerFactoryProvider.java b/src/main/java/net/woggioni/gbcs/url/ClasspathUrlStreamHandlerFactoryProvider.java new file mode 100644 index 0000000..5263a27 --- /dev/null +++ b/src/main/java/net/woggioni/gbcs/url/ClasspathUrlStreamHandlerFactoryProvider.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt b/src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt index faf2c72..18afac7 100644 --- a/src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt +++ b/src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt @@ -16,7 +16,9 @@ import javax.net.ssl.TrustManagerFactory 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) { if (evt is SslHandshakeCompletionEvent) { if (evt.isSuccess) { @@ -32,20 +34,17 @@ class ClientCertificateValidator private constructor(private val sslHandler : Ss } companion object { - - fun of(sslHandler : SslHandler, trustStore : KeyStore?) : ClientCertificateValidator { - val certificateFactory = CertificateFactory.getInstance("X.509") - - val validator = CertPathValidator.getInstance("PKIX").apply { - val rc = revocationChecker as PKIXRevocationChecker - rc.options = EnumSet.of( - PKIXRevocationChecker.Option.NO_FALLBACK, - PKIXRevocationChecker.Option.SOFT_FAIL, - PKIXRevocationChecker.Option.PREFER_CRLS) - } - - val manager = if(trustStore != null) { - val params = PKIXParameters(trustStore) + fun getTrustManager(trustStore : KeyStore?, certificateRevocationEnabled : Boolean) : X509TrustManager { + return if(trustStore != null) { + val certificateFactory = CertificateFactory.getInstance("X.509") + val validator = CertPathValidator.getInstance("PKIX").apply { + val rc = revocationChecker as PKIXRevocationChecker + rc.options = EnumSet.of( + PKIXRevocationChecker.Option.NO_FALLBACK) + } + val params = PKIXParameters(trustStore).apply { + isRevocationEnabled = certificateRevocationEnabled + } object : X509TrustManager { override fun checkClientTrusted(chain: Array, authType: String) { val clientCertificateChain = certificateFactory.generateCertPath(chain.toList()) @@ -56,15 +55,23 @@ class ClientCertificateValidator private constructor(private val sslHandler : Ss throw NotImplementedError() } - override fun getAcceptedIssuers(): Array { - throw NotImplementedError() - } + private val acceptedIssuers = trustStore.aliases().asSequence() + .filter (trustStore::isCertificateEntry) + .map(trustStore::getCertificate) + .map { it as X509Certificate } + .toList() + .toTypedArray() + + override fun getAcceptedIssuers() = acceptedIssuers } } else { val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) 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)) } } } \ No newline at end of file diff --git a/src/main/kotlin/net/woggioni/gbcs/Configuration.kt b/src/main/kotlin/net/woggioni/gbcs/Configuration.kt index b1f1e53..174da34 100644 --- a/src/main/kotlin/net/woggioni/gbcs/Configuration.kt +++ b/src/main/kotlin/net/woggioni/gbcs/Configuration.kt @@ -21,6 +21,7 @@ data class KeyStore( data class TrustStore( val file : Path, val password : String?, + val checkCertificateStatus : Boolean ) data class Configuration( @@ -28,18 +29,26 @@ data class Configuration( val host : String, val port : Int, val users : Map>, + val groups : Map>, val tlsConfiguration: TlsConfiguration?, - val serverPath : String + val serverPath : String, + val useVirtualThread : Boolean ) { companion object { fun parse(document : Element) : Configuration { var cacheFolder = Paths.get(System.getProperty("user.home")).resolve(".gbcs") - var host : String = "127.0.0.1" - var port : Int = 11080 - var users = emptyMap>() + var host = "127.0.0.1" + var port = 11080 + val users = emptyMap>() + val groups = emptyMap>() 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()) { when(child.nodeName) { @@ -62,8 +71,8 @@ data class Configuration( val trustStoreFile = Paths.get(granChild.getAttribute("file")) val trustStorePassword = granChild.getAttribute("password") .takeIf(String::isNotEmpty) - val keyAlias = granChild.getAttribute("server-key-alias") - val keyPasswordPassword = granChild.getAttribute("server-key-password") + val keyAlias = granChild.getAttribute("key-alias") + val keyPasswordPassword = granChild.getAttribute("password") .takeIf(String::isNotEmpty) keyStore = KeyStore( trustStoreFile, @@ -76,9 +85,14 @@ data class Configuration( 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 + 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) } } } diff --git a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt index 3027ef3..81701e6 100644 --- a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt @@ -13,13 +13,15 @@ import java.security.MessageDigest import java.security.PrivateKey import java.security.cert.X509Certificate import java.util.AbstractMap.SimpleEntry +import java.util.Arrays import java.util.Base64 -import java.util.ServiceLoader +import java.util.concurrent.Executors import io.netty.bootstrap.ServerBootstrap import io.netty.buffer.ByteBuf import io.netty.buffer.Unpooled import io.netty.channel.Channel import io.netty.channel.ChannelDuplexHandler +import io.netty.channel.ChannelFuture import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInitializer @@ -30,11 +32,13 @@ import io.netty.channel.EventLoopGroup import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.nio.NioEventLoopGroup 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.http.DefaultFullHttpResponse import io.netty.handler.codec.http.DefaultHttpContent import io.netty.handler.codec.http.DefaultHttpResponse 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.HttpHeaderNames 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.HttpServerCodec 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.ssl.ClientAuth +import io.netty.handler.ssl.SslContext import io.netty.handler.ssl.SslContextBuilder -import io.netty.handler.ssl.util.SelfSignedCertificate import io.netty.handler.stream.ChunkedNioFile import io.netty.handler.stream.ChunkedWriteHandler import io.netty.util.concurrent.DefaultEventExecutorGroup +import io.netty.util.concurrent.DefaultThreadFactory 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.JWO import net.woggioni.jwo.Tuple2 +import java.net.URI +import java.util.concurrent.ForkJoinPool class GradleBuildCacheServer(private val cfg : Configuration) { @@ -125,6 +136,33 @@ class GradleBuildCacheServer(private val cfg : Configuration) { private class ServerInitializer(private val cfg : Configuration) : ChannelInitializer() { + 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 = Arrays.stream(javaKeyStore.getCertificateChain(keyStore.keyAlias)) + .map { it as X509Certificate } + .toArray {size -> Array(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 { val group: EventExecutorGroup = DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors()) fun loadKeystore(file : Path, password : String?) : KeyStore { @@ -132,13 +170,13 @@ class GradleBuildCacheServer(private val cfg : Configuration) { .map(Tuple2::get_2) .orElseThrow { 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") "p12", "pfx" -> KeyStore.getInstance("PKCS12") 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 { keystore.load(it, password?.let(String::toCharArray)) @@ -148,34 +186,17 @@ class GradleBuildCacheServer(private val cfg : Configuration) { } override fun initChannel(ch: Channel) { + val userAuthorizer = UserAuthorizer(cfg.users) val pipeline = ch.pipeline() - val tlsConfiguration = cfg.tlsConfiguration - if(tlsConfiguration != null) { - 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 - SslContextBuilder.forServer(serverKey, *serverCert).build() - } - val sslHandler = sslCtx.newHandler(ch.alloc()) + if(sslContext != null) { + val sslHandler = sslContext.newHandler(ch.alloc()) 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(HttpChunkContentCompressor(1024)) pipeline.addLast(ChunkedWriteHandler()) 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(ExceptionHandler()) Files.createDirectories(cfg.cacheFolder) @@ -184,9 +205,25 @@ class GradleBuildCacheServer(private val cfg : Configuration) { private class ExceptionHandler : ChannelDuplexHandler() { 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) { - log.error(cause.message, cause) - ctx.close() + when(cause) { + 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() { - // Create the multithreaded event loops for the server - val bossGroup: EventLoopGroup = NioEventLoopGroup() - val workerGroup: EventLoopGroup = NioEventLoopGroup() - try { - // A helper class that simplifies server configuration - val httpBootstrap = ServerBootstrap() + class ServerHandle( + private val httpChannel : ChannelFuture, + private val bossGroup: EventLoopGroup, + private val workerGroup: EventLoopGroup) : AutoCloseable { - // Configure the server - httpBootstrap.group(bossGroup, workerGroup) - .channel(NioServerSocketChannel::class.java) - .childHandler(ServerInitializer(cfg)) - .option(ChannelOption.SO_BACKLOG, 128) - .childOption(ChannelOption.SO_KEEPALIVE, true) + private val closeFuture : ChannelFuture = httpChannel.channel().closeFuture() - // Bind and start to accept incoming connections. - val bindAddress = InetSocketAddress(cfg.host, cfg.port) - val httpChannel = httpBootstrap.bind(bindAddress).sync() - - // Wait until server socket is closed - httpChannel.channel().closeFuture().sync() - } finally { - workerGroup.shutdownGracefully() - bossGroup.shutdownGracefully() + fun shutdown() : ChannelFuture { + return httpChannel.channel().close() } + + 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 { + private val log by lazy { + contextLogger() + } private const val PROTOCOL_HANDLER = "java.protocol.handler.pkgs" private const val HANDLERS_PACKAGE = "net.woggioni.gbcs.url" @@ -341,13 +405,9 @@ class GradleBuildCacheServer(private val cfg : Configuration) { resetCachedUrlHandlers() } - @JvmStatic - fun main(args: Array) { - SelfSignedCertificate() - ServiceLoader.load(javaClass.module.layer, URLStreamHandlerFactory::class.java).stream().forEach { - println(it.type()) - } -// registerUrlProtocolHandler() + fun loadConfiguration(args: Array) : Configuration { + registerUrlProtocolHandler() + URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider()) Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader val app = Application.builder("gbcs") .configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR") @@ -355,30 +415,39 @@ class GradleBuildCacheServer(private val cfg : Configuration) { .build() val confDir = app.computeConfigurationDirectory() val configurationFile = confDir.resolve("gbcs.xml") - + log.info { "here" } if(!Files.exists(configurationFile)) { Files.createDirectories(confDir) - val defaultConfigurationFileResourcePath = "net/woggioni/gbcs/gbcs-default.xml" - val defaultConfigurationFileResource = GradleBuildCacheServer.javaClass.classLoader - .getResource(defaultConfigurationFileResourcePath) - ?: throw IllegalStateException( - "Missing default configuration file 'classpath:$defaultConfigurationFileResourcePath'") + val defaultConfigurationFileResourcePath = "classpath:net/woggioni/gbcs/gbcs-default.xml" + val defaultConfigurationFileResource = URI(defaultConfigurationFileResourcePath).toURL() Files.newOutputStream(configurationFile).use { outputStream -> defaultConfigurationFileResource.openStream().use { inputStream -> JWO.copy(inputStream, outputStream) } } } - val schemaResource = "net/woggioni/gbcs/gbcs.xsd" - val schemaUrl = URL("classpath:net/woggioni/gbcs/gbcs.xsd") -// val schemaUrl = GradleBuildCacheServer::class.java.classLoader.getResource(schemaResource) -// ?: throw IllegalStateException("Missing configuration schema '$schemaResource'") - val schemaUrl2 = URL(schemaUrl.toString()) +// val schemaUrl = javaClass.getResource("/net/woggioni/gbcs/gbcs.xsd") + val schemaUrl = URL.of(URI("classpath:net/woggioni/gbcs/gbcs.xsd"), null) val dbf = Xml.newDocumentBuilderFactory() +// dbf.schema = Xml.getSchema(this::class.java.module.getResourceAsStream("/net/woggioni/gbcs/gbcs.xsd")) dbf.schema = Xml.getSchema(schemaUrl) - val doc = Files.newInputStream(configurationFile) - .use(dbf.newDocumentBuilder()::parse) - GradleBuildCacheServer(Configuration.parse(doc.documentElement)).run() + val db = dbf.newDocumentBuilder().apply { + setErrorHandler(Xml.ErrorHandler(schemaUrl)) + } + val doc = Files.newInputStream(configurationFile).use(db::parse) + return Configuration.parse(doc.documentElement) + } + + @JvmStatic + fun main(args: Array) { + 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, @@ -392,4 +461,14 @@ class GradleBuildCacheServer(private val cfg : Configuration) { return JWO.bytesToHex(digest(data, md)) } } +} + +object GraalNativeImageConfiguration { + @JvmStatic + fun main(args: Array) { + val conf = GradleBuildCacheServer.loadConfiguration(args) + val handle = GradleBuildCacheServer(conf).run() + Thread.sleep(10_000) + handle.shutdown() + } } \ No newline at end of file diff --git a/src/main/kotlin/net/woggioni/gbcs/Logging.kt b/src/main/kotlin/net/woggioni/gbcs/Logging.kt index 081a0f5..a2e922d 100644 --- a/src/main/kotlin/net/woggioni/gbcs/Logging.kt +++ b/src/main/kotlin/net/woggioni/gbcs/Logging.kt @@ -8,9 +8,6 @@ import java.nio.file.Files import java.nio.file.Path import java.util.logging.LogManager - - - inline fun T.contextLogger() = LoggerFactory.getLogger(T::class.java) inline fun Logger.traceParam(messageBuilder : () -> Pair>) { diff --git a/src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt b/src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt new file mode 100644 index 0000000..51e4121 --- /dev/null +++ b/src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt @@ -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>) : 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 +} \ No newline at end of file diff --git a/src/main/kotlin/net/woggioni/gbcs/Xml.kt b/src/main/kotlin/net/woggioni/gbcs/Xml.kt index 7e3dec7..cbf00e3 100644 --- a/src/main/kotlin/net/woggioni/gbcs/Xml.kt +++ b/src/main/kotlin/net/woggioni/gbcs/Xml.kt @@ -1,5 +1,7 @@ package net.woggioni.gbcs +import java.io.InputStream +import java.io.InputStreamReader import java.net.URL import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD 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.parsers.DocumentBuilder 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.SchemaFactory +import net.woggioni.jwo.xml.Xml 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.ErrorHandler +import org.xml.sax.ErrorHandler as ErrHandler import org.xml.sax.SAXNotRecognizedException import org.xml.sax.SAXNotSupportedException import org.xml.sax.SAXParseException @@ -66,10 +71,10 @@ class ElementIterator(parent: Element, name: String? = null) : Iterator object Xml { - private class XmlErrorHandler(private val fileURL: URL) : ErrorHandler { + class ErrorHandler(private val fileURL: URL) : ErrHandler { companion object { - private val log = LoggerFactory.getLogger(XmlErrorHandler::class.java) + private val log = LoggerFactory.getLogger(ErrorHandler::class.java) } override fun warning(ex: SAXParseException) { @@ -118,9 +123,18 @@ object Xml { 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(): DocumentBuilderFactory { val dbf = DocumentBuilderFactory.newInstance() dbf.setFeature(FEATURE_SECURE_PROCESSING, true) @@ -129,6 +143,7 @@ object Xml { dbf.isExpandEntityReferences = false dbf.isIgnoringComments = true dbf.isNamespaceAware = true + dbf.isValidating = false return dbf } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..b562cd5 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,18 @@ + + + + + + + + + System.out + + %d [%highlight(%-5level)] \(%thread\) %logger{36} -%kvp- %msg %n + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties index 744af31..e4725b4 100644 --- a/src/main/resources/logging.properties +++ b/src/main/resources/logging.properties @@ -2,8 +2,8 @@ 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.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 = \ No newline at end of file diff --git a/src/main/resources/net/woggioni/gbcs/gbcs-default.xml b/src/main/resources/net/woggioni/gbcs/gbcs-default.xml index d64444f..f5878bf 100644 --- a/src/main/resources/net/woggioni/gbcs/gbcs-default.xml +++ b/src/main/resources/net/woggioni/gbcs/gbcs-default.xml @@ -1,14 +1,48 @@ - + - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/net/woggioni/gbcs/gbcs.xsd b/src/main/resources/net/woggioni/gbcs/gbcs.xsd index 4ca05dd..ce3adfe 100644 --- a/src/main/resources/net/woggioni/gbcs/gbcs.xsd +++ b/src/main/resources/net/woggioni/gbcs/gbcs.xsd @@ -1,17 +1,28 @@ - + xmlns:gbcs="urn:net.woggioni.gbcs"> - - - - + + + + + + + + + + + + + + + @@ -23,26 +34,92 @@ - + + + + + + + + + + + + + - - + + + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -58,6 +135,7 @@ + diff --git a/src/test/kotlin/net/woggioni/gbcs/CertificateValidationTest.kt b/src/test/kotlin/net/woggioni/gbcs/CertificateValidationTest.kt new file mode 100644 index 0000000..d4a9c74 --- /dev/null +++ b/src/test/kotlin/net/woggioni/gbcs/CertificateValidationTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/woggioni/gbcs/ConfigurationTest.kt b/src/test/kotlin/net/woggioni/gbcs/ConfigurationTest.kt new file mode 100644 index 0000000..f78c1f1 --- /dev/null +++ b/src/test/kotlin/net/woggioni/gbcs/ConfigurationTest.kt @@ -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) + } +} \ No newline at end of file