temporary commit
This commit is contained in:
62
build.gradle
62
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<EnvelopeJarTask> 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'
|
||||
|
@@ -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
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
35
gradlew
vendored
35
gradlew
vendored
@@ -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" \
|
||||
|
21
gradlew.bat
vendored
21
gradlew.bat
vendored
@@ -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
|
||||
|
||||
|
1
native-image/native-image.properties
Normal file
1
native-image/native-image.properties
Normal file
@@ -0,0 +1 @@
|
||||
Args=-H:Optimize=3 -H:+TraceClassInitialization
|
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
88
src/main/java/net/woggioni/gbcs/NettyPingServer.java
Normal file
88
src/main/java/net/woggioni/gbcs/NettyPingServer.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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<out X509Certificate>, 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<X509Certificate> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
@@ -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<String, Set<Role>>,
|
||||
val groups : Map<String, Set<Role>>,
|
||||
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<String, Set<Role>>()
|
||||
var host = "127.0.0.1"
|
||||
var port = 11080
|
||||
val users = emptyMap<String, Set<Role>>()
|
||||
val groups = emptyMap<String, Set<Role>>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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<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 {
|
||||
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<String, String>::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<X509Certificate>
|
||||
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<String>) {
|
||||
SelfSignedCertificate()
|
||||
ServiceLoader.load(javaClass.module.layer, URLStreamHandlerFactory::class.java).stream().forEach {
|
||||
println(it.type())
|
||||
}
|
||||
// registerUrlProtocolHandler()
|
||||
fun loadConfiguration(args: Array<String>) : Configuration {
|
||||
registerUrlProtocolHandler()
|
||||
URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
|
||||
Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader
|
||||
val app = Application.builder("gbcs")
|
||||
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
|
||||
@@ -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<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,
|
||||
@@ -392,4 +461,14 @@ class GradleBuildCacheServer(private val cfg : Configuration) {
|
||||
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()
|
||||
}
|
||||
}
|
@@ -8,9 +8,6 @@ import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.logging.LogManager
|
||||
|
||||
|
||||
|
||||
|
||||
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
|
||||
|
||||
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {
|
||||
|
22
src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt
Normal file
22
src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt
Normal 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
|
||||
}
|
@@ -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<Element>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
18
src/main/resources/logback.xml
Normal file
18
src/main/resources/logback.xml
Normal 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>
|
@@ -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 =
|
@@ -1,14 +1,48 @@
|
||||
<?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"/>
|
||||
<cache path="/tmp/gbcs"/>
|
||||
<groups>
|
||||
<readers>
|
||||
<!-- <user name="reader"/>-->
|
||||
</readers>
|
||||
<writers>
|
||||
<!-- <user name="writer"/>-->
|
||||
</writers>
|
||||
</groups>
|
||||
<tls verify-clients="true"/>
|
||||
</server>
|
||||
<authorization>
|
||||
<users>
|
||||
<user name="user1" password="password"/>
|
||||
<user name="user2" password="password"/>
|
||||
<user name="user3" password="password"/>
|
||||
<user name="user4" password="password"/>
|
||||
<user name="user5" password="password">
|
||||
<roles>
|
||||
<reader/>
|
||||
<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>
|
@@ -1,17 +1,28 @@
|
||||
<?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:gbcs="urn:gradle-build-cache-server">
|
||||
xmlns:gbcs="urn:net.woggioni.gbcs">
|
||||
<xs:element name="server" type="gbcs:serverType"/>
|
||||
|
||||
<xs:complexType name="serverType">
|
||||
<xs:sequence minOccurs="0">
|
||||
<xs:element name="bind" type="gbcs:bindType"/>
|
||||
<xs:element name="cache" type="gbcs:cacheDirType"/>
|
||||
<xs:element name="groups" type="gbcs:groupsType"/>
|
||||
<xs:element name="tls" type="gbcs:tlsType"/>
|
||||
<xs:element name="bind" type="gbcs:bindType" maxOccurs="1"/>
|
||||
<xs:element name="cache" type="gbcs:cacheDirType" maxOccurs="1"/>
|
||||
<xs:element name="authorization" type="gbcs:authorizationType" maxOccurs="1">
|
||||
<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:attribute name="path" type="xs:string"/>
|
||||
<xs:attribute name="useVirtualThreads" type="xs:boolean" use="optional"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="bindType">
|
||||
@@ -23,26 +34,92 @@
|
||||
<xs:attribute name="path" type="xs:string" use="required"/>
|
||||
</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:element name="readers" type="gbcs:groupType"/>
|
||||
<xs:element name="writers" type="gbcs:groupType"/>
|
||||
<xs:element name="users" type="gbcs:usersType"/>
|
||||
<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:complexType>
|
||||
|
||||
<xs:complexType name="groupType">
|
||||
<xs:complexType name="usersType">
|
||||
<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:complexType>
|
||||
|
||||
<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="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 name="tlsType">
|
||||
<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:all>
|
||||
<xs:attribute name="verify-clients" type="xs:boolean" use="optional"/>
|
||||
@@ -58,6 +135,7 @@
|
||||
<xs:complexType name="trustStoreType">
|
||||
<xs:attribute name="file" type="xs:string" use="required"/>
|
||||
<xs:attribute name="password" type="xs:string"/>
|
||||
<xs:attribute name="check-certificate-status" type="xs:boolean"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="propertiesType">
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
22
src/test/kotlin/net/woggioni/gbcs/ConfigurationTest.kt
Normal file
22
src/test/kotlin/net/woggioni/gbcs/ConfigurationTest.kt
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user