Compare commits

...

8 Commits
0.0.4 ... 0.0.8

Author SHA1 Message Date
75ebf2248f general refactoring
All checks were successful
CI / build (push) Successful in 2m30s
2025-01-17 14:17:57 +08:00
241d95fe1c added env variable and java properties substitution in configuration attributes
All checks were successful
CI / build (push) Successful in 3m29s
2025-01-16 21:11:35 +08:00
3b7030c302 fixed mainClassName for native image build
All checks were successful
CI / build (push) Successful in 3m22s
2025-01-16 17:23:03 +08:00
a8670277e7 fixed XML error handler for server command 2025-01-16 17:01:14 +08:00
03ee75266d added average turnaround time calculation in benchmark 2025-01-16 14:54:21 +08:00
05a265e4b4 added connection pooling to gbcs-client
All checks were successful
CI / build (push) Successful in 3m55s
2025-01-16 13:37:14 +08:00
5af99330f8 server is now a subcommand 2025-01-16 11:35:05 +08:00
747168cda3 added client command 2025-01-16 11:16:01 +08:00
81 changed files with 1089 additions and 642 deletions

View File

@@ -1,26 +0,0 @@
plugins {
alias catalog.plugins.gradle.jmh
alias catalog.plugins.lombok
}
import me.champeau.jmh.JMHTask
dependencies {
implementation rootProject
implementation catalog.jwo
implementation catalog.xz
implementation catalog.jackson.databind
jmhAnnotationProcessor catalog.lombok
}
jmh {
threads = 4
iterations = 2
fork = 1
warmupIterations = 1
warmupForks = 0
resultFormat = 'JSON'
}

View File

@@ -1,262 +0,0 @@
package net.woggioni.gbcs.benchmark;
import lombok.Getter;
import lombok.SneakyThrows;
import net.woggioni.jwo.Fun;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class Main {
@SneakyThrows
private static Properties loadProperties() {
Properties properties = new Properties();
try (final var is = Main.class.getResourceAsStream("/benchmark.properties")) {
properties.load(is);
}
return properties;
}
private static final Properties properties = loadProperties();
@State(Scope.Thread)
public static class ExecutionPlan {
private final Random random = new Random(101325);
@Getter
private final HttpClient client = createHttpClient();
private final Map<String, byte[]> entries = new HashMap<>();
private HttpClient createHttpClient() {
final var clientBuilder = HttpClient.newBuilder();
getSslContext().ifPresent(clientBuilder::sslContext);
return clientBuilder.build();
}
public final Map<String, byte[]> getEntries() {
return Collections.unmodifiableMap(entries);
}
public Map.Entry<String, byte[]> newEntry() {
final var keyBuffer = new byte[0x20];
random.nextBytes(keyBuffer);
final var key = Base64.getUrlEncoder().encodeToString(keyBuffer);
final var value = new byte[0x1000];
random.nextBytes(value);
return Map.entry(key, value);
}
@SneakyThrows
public HttpRequest.Builder newRequestBuilder(String key) {
final var requestBuilder = HttpRequest.newBuilder()
.uri(getServerURI().resolve(key));
String user = getUser();
if (user != null) {
requestBuilder.header("Authorization", buildAuthorizationHeader(user, getPassword()));
}
return requestBuilder;
}
@SneakyThrows
public URI getServerURI() {
return new URI(properties.getProperty("gbcs.server.url"));
}
@SneakyThrows
public Optional<String> getClientTrustStorePassword() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.password"))
.filter(Predicate.not(String::isEmpty));
}
@SneakyThrows
public Optional<KeyStore> getClientTrustStore() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.file"))
.filter(Predicate.not(String::isEmpty))
.map(Path::of)
.map((Fun<Path, KeyStore>) keyStoreFile -> {
final var keyStore = KeyStore.getInstance("PKCS12");
try (final var is = Files.newInputStream(keyStoreFile)) {
keyStore.load(is, getClientTrustStorePassword().map(String::toCharArray).orElse(null));
}
return keyStore;
});
}
@SneakyThrows
public Optional<KeyStore> getClientKeyStore() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.file"))
.filter(Predicate.not(String::isEmpty))
.map(Path::of)
.map((Fun<Path, KeyStore>) keyStoreFile -> {
final var keyStore = KeyStore.getInstance("PKCS12");
try (final var is = Files.newInputStream(keyStoreFile)) {
keyStore.load(is, getClientKeyStorePassword().map(String::toCharArray).orElse(null));
}
return keyStore;
});
}
@SneakyThrows
public Optional<String> getClientKeyStorePassword() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.password"))
.filter(Predicate.not(String::isEmpty));
}
@SneakyThrows
public Optional<String> getClientKeyPassword() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.key.password"))
.filter(Predicate.not(String::isEmpty));
}
@SneakyThrows
public String getUser() {
return Optional.ofNullable(properties.getProperty("gbcs.server.username"))
.filter(Predicate.not(String::isEmpty))
.orElse(null);
}
@SneakyThrows
public String getPassword() {
return Optional.ofNullable(properties.getProperty("gbcs.server.password"))
.filter(Predicate.not(String::isEmpty))
.orElse(null);
}
private String buildAuthorizationHeader(String user, String password) {
final var b64 = Base64.getEncoder().encode(String.format("%s:%s", user, password).getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(b64);
}
@SneakyThrows
private Optional<SSLContext> getSslContext() {
return getClientKeyStore().map((Fun<KeyStore, SSLContext>) clientKeyStore -> {
final var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientKeyStore, getClientKeyStorePassword().map(String::toCharArray).orElse(null));
// Set up trust manager factory with the truststore
final var trustManagers = getClientTrustStore().map((Fun<KeyStore, TrustManager[]>) ts -> {
final var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
return tmf.getTrustManagers();
}).orElse(new TrustManager[0]);
// Create SSL context with the key and trust managers
final var sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), trustManagers, null);
return sslContext;
});
}
@SneakyThrows
@Setup(Level.Trial)
public void setUp() {
final var client = getClient();
for (int i = 0; i < 1000; i++) {
final var pair = newEntry();
final var requestBuilder = newRequestBuilder(pair.getKey())
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(pair.getValue()));
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
if (201 != response.statusCode()) {
throw new IllegalStateException(Integer.toString(response.statusCode()));
} else {
entries.put(pair.getKey(), pair.getValue());
}
}
}
@TearDown
public void tearDown() {
client.close();
}
private Iterator<Map.Entry<String, byte[]>> it = null;
private Map.Entry<String, byte[]> nextEntry() {
if (it == null || !it.hasNext()) {
it = getEntries().entrySet().iterator();
}
return it.next();
}
}
@SneakyThrows
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public void get(ExecutionPlan plan) {
final var client = plan.getClient();
final var entry = plan.nextEntry();
final var requestBuilder = plan.newRequestBuilder(entry.getKey())
.header("Accept", "application/octet-stream")
.GET();
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
if (200 != response.statusCode()) {
throw new IllegalStateException(Integer.toString(response.statusCode()));
} else {
if (!Arrays.equals(entry.getValue(), response.body())) {
throw new IllegalStateException("Retrieved unexpected value");
}
}
}
@SneakyThrows
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public void put(Main.ExecutionPlan plan) {
final var client = plan.getClient();
final var entry = plan.nextEntry();
final var requestBuilder = plan.newRequestBuilder(entry.getKey())
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(entry.getValue()));
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
if (201 != response.statusCode()) {
throw new IllegalStateException(Integer.toString(response.statusCode()));
}
}
}

View File

@@ -1 +0,0 @@
gbcs.server.url= http://localhost:8080

View File

@@ -1,14 +1,12 @@
plugins { plugins {
id 'java-library' alias catalog.plugins.kotlin.jvm apply false
alias catalog.plugins.kotlin.jvm
alias catalog.plugins.sambal alias catalog.plugins.sambal
alias catalog.plugins.lombok alias catalog.plugins.lombok apply false
id 'maven-publish'
} }
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
allprojects { subproject -> allprojects { subproject ->
group = 'net.woggioni' group = 'net.woggioni'
@@ -102,34 +100,6 @@ allprojects { subproject ->
} }
} }
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.netty.codec.http
api project('gbcs-base')
api project('gbcs-api')
// runtimeOnly catalog.slf4j.jdk14
testRuntimeOnly catalog.logback.classic
testImplementation catalog.bcprov.jdk18on
testImplementation catalog.bcpkix.jdk18on
testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine
testRuntimeOnly project("gbcs-memcached")
}
publishing {
publications {
maven(MavenPublication) {
from(components["java"])
}
}
}
tasks.register('version') { tasks.register('version') {
doLast { doLast {
println("VERSION=$version") println("VERSION=$version")

View File

@@ -7,15 +7,15 @@ WORKDIR /home/luser
FROM base-release AS release FROM base-release AS release
ADD gbcs-cli-envelope-*.jar gbcs.jar ADD gbcs-cli-envelope-*.jar gbcs.jar
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar"] ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]
FROM base-release AS release-memcached FROM base-release AS release-memcached
ADD --chown=luser:luser gbcs-cli-envelope-*.jar gbcs.jar ADD --chown=luser:luser gbcs-cli-envelope-*.jar gbcs.jar
RUN mkdir plugins RUN mkdir plugins
WORKDIR /home/luser/plugins WORKDIR /home/luser/plugins
RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/gbcs-memcached*.tar RUN --mount=type=bind,source=.,target=/build/distributions tar -xf /build/distributions/gbcs-server-memcached*.tar
WORKDIR /home/luser WORKDIR /home/luser
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar"] ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar", "server"]
FROM release-memcached as compose FROM release-memcached as compose
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml

View File

@@ -19,7 +19,7 @@ configurations {
dependencies { dependencies {
docker project(path: ':gbcs-cli', configuration: 'release') docker project(path: ':gbcs-cli', configuration: 'release')
docker project(path: ':gbcs-memcached', configuration: 'release') docker project(path: ':gbcs-server-memcached', configuration: 'release')
} }
Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {} Provider<Task> cleanTaskProvider = tasks.named(BasePlugin.CLEAN_TASK_NAME) {}

View File

@@ -0,0 +1,11 @@
package net.woggioni.gbcs.api.exception;
public class ConfigurationException extends GbcsException {
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
public ConfigurationException(String message) {
this(message, null);
}
}

View File

@@ -4,15 +4,17 @@ plugins {
alias catalog.plugins.envelope alias catalog.plugins.envelope
alias catalog.plugins.sambal alias catalog.plugins.sambal
alias catalog.plugins.graalvm.native.image alias catalog.plugins.graalvm.native.image
alias catalog.plugins.graalvm.jlink
alias catalog.plugins.jpms.check
id 'maven-publish' id 'maven-publish'
} }
import net.woggioni.gradle.envelope.EnvelopeJarTask import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImagePlugin import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.NativeImageTask import net.woggioni.gradle.graalvm.NativeImageTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask import net.woggioni.gradle.graalvm.JlinkPlugin
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import net.woggioni.gradle.graalvm.JlinkTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
Property<String> mainClassName = objects.property(String.class) Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli') mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
@@ -43,7 +45,8 @@ dependencies {
implementation catalog.netty.codec.http implementation catalog.netty.codec.http
implementation catalog.picocli implementation catalog.picocli
implementation rootProject implementation project(':gbcs-client')
implementation project(':gbcs-server')
// runtimeOnly catalog.slf4j.jdk14 // runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic runtimeOnly catalog.logback.classic
@@ -56,15 +59,20 @@ Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', E
} }
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) { tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
mainClass = 'net.woggioni.gbcs.GraalNativeImageConfiguration' mainClass = mainClassName
} }
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) { tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer' mainClass = mainClassName
useMusl = true useMusl = true
buildStaticImage = true buildStaticImage = true
} }
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
mainClass = mainClassName
mainModule = 'net.woggioni.gbcs.cli'
}
artifacts { artifacts {
release(envelopeJarTaskProvider) release(envelopeJarTaskProvider)
} }

View File

@@ -1,15 +1,17 @@
module net.woggioni.gbcs.cli { module net.woggioni.gbcs.cli {
requires org.slf4j; requires org.slf4j;
requires net.woggioni.gbcs; requires net.woggioni.gbcs.server;
requires info.picocli; requires info.picocli;
requires net.woggioni.gbcs.base; requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.client;
requires kotlin.stdlib; requires kotlin.stdlib;
requires net.woggioni.jwo; requires net.woggioni.jwo;
requires net.woggioni.gbcs.api;
exports net.woggioni.gbcs.cli.impl.converters to info.picocli; exports net.woggioni.gbcs.cli.impl.converters to info.picocli;
opens net.woggioni.gbcs.cli.impl.commands to info.picocli; opens net.woggioni.gbcs.cli.impl.commands to info.picocli;
opens net.woggioni.gbcs.cli.impl to info.picocli; opens net.woggioni.gbcs.cli.impl to info.picocli;
opens net.woggioni.gbcs.cli to info.picocli, net.woggioni.gbcs.base; opens net.woggioni.gbcs.cli to info.picocli, net.woggioni.gbcs.common;
exports net.woggioni.gbcs.cli; exports net.woggioni.gbcs.cli;
} }

View File

@@ -1,28 +1,24 @@
package net.woggioni.gbcs.cli package net.woggioni.gbcs.cli
import net.woggioni.gbcs.GradleBuildCacheServer import net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.cli.impl.AbstractVersionProvider import net.woggioni.gbcs.cli.impl.AbstractVersionProvider
import net.woggioni.gbcs.cli.impl.GbcsCommand import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.commands.BenchmarkCommand
import net.woggioni.gbcs.cli.impl.commands.ClientCommand
import net.woggioni.gbcs.cli.impl.commands.GetCommand
import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand
import net.woggioni.gbcs.cli.impl.commands.PutCommand
import net.woggioni.gbcs.cli.impl.commands.ServerCommand
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import org.slf4j.Logger
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Model.CommandSpec
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command( @CommandLine.Command(
name = "gbcs", versionProvider = GradleBuildCacheServerCli.VersionProvider::class name = "gbcs", versionProvider = GradleBuildCacheServerCli.VersionProvider::class
) )
class GradleBuildCacheServerCli(application : Application, private val log : Logger) : GbcsCommand() { class GradleBuildCacheServerCli : GbcsCommand() {
class VersionProvider : AbstractVersionProvider() class VersionProvider : AbstractVersionProvider()
companion object { companion object {
@@ -35,24 +31,24 @@ class GradleBuildCacheServerCli(application : Application, private val log : Log
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR") .configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
.configurationDirectoryPropertyKey("net.woggioni.gbcs.conf.dir") .configurationDirectoryPropertyKey("net.woggioni.gbcs.conf.dir")
.build() .build()
val gbcsCli = GradleBuildCacheServerCli(app, log) val gbcsCli = GradleBuildCacheServerCli()
val commandLine = CommandLine(gbcsCli) val commandLine = CommandLine(gbcsCli)
commandLine.setExecutionExceptionHandler { ex, cl, parseResult -> commandLine.setExecutionExceptionHandler { ex, cl, parseResult ->
log.error(ex.message, ex) log.error(ex.message, ex)
CommandLine.ExitCode.SOFTWARE CommandLine.ExitCode.SOFTWARE
} }
commandLine.addSubcommand(ServerCommand(app))
commandLine.addSubcommand(PasswordHashCommand()) commandLine.addSubcommand(PasswordHashCommand())
commandLine.addSubcommand(
CommandLine(ClientCommand(app)).apply {
addSubcommand(BenchmarkCommand())
addSubcommand(PutCommand())
addSubcommand(GetCommand())
})
System.exit(commandLine.execute(*args)) System.exit(commandLine.execute(*args))
} }
} }
@CommandLine.Option(
names = ["-c", "--config-file"],
description = ["Read the application configuration from this file"],
paramLabel = "CONFIG_FILE"
)
private var configurationFile: Path = findConfigurationFile(application)
@CommandLine.Option(names = ["-V", "--version"], versionHelp = true) @CommandLine.Option(names = ["-V", "--version"], versionHelp = true)
var versionHelp = false var versionHelp = false
private set private set
@@ -60,40 +56,8 @@ class GradleBuildCacheServerCli(application : Application, private val log : Log
@CommandLine.Spec @CommandLine.Spec
private lateinit var spec: CommandSpec private lateinit var spec: CommandSpec
private fun findConfigurationFile(app : Application): Path {
val confDir = app.computeConfigurationDirectory()
val configurationFile = confDir.resolve("gbcs.xml")
return configurationFile
}
private fun createDefaultConfigurationFile(configurationFile : Path) {
log.info {
"Creating default configuration file at '$configurationFile'"
}
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
Files.newOutputStream(configurationFile).use { outputStream ->
defaultConfigurationFileResource.openStream().use { inputStream ->
JWO.copy(inputStream, outputStream)
}
}
}
override fun run() { override fun run() {
if (!Files.exists(configurationFile)) { spec.commandLine().usage(System.out);
Files.createDirectories(configurationFile.parent)
createDefaultConfigurationFile(configurationFile)
}
val configuration = GradleBuildCacheServer.loadConfiguration(configurationFile)
log.debug {
ByteArrayOutputStream().also {
GradleBuildCacheServer.dumpConfiguration(configuration, it)
}.let {
"Server configuration:\n${String(it.toByteArray())}"
}
}
val server = GradleBuildCacheServer(configuration)
server.run().use {
}
} }
} }

View File

@@ -1,8 +1,6 @@
package net.woggioni.gbcs.cli.impl package net.woggioni.gbcs.cli.impl
import picocli.CommandLine import picocli.CommandLine
import java.net.URL
import java.util.Enumeration
import java.util.jar.Attributes import java.util.jar.Attributes
import java.util.jar.JarFile import java.util.jar.JarFile
import java.util.jar.Manifest import java.util.jar.Manifest

View File

@@ -1,6 +1,8 @@
package net.woggioni.gbcs.cli.impl package net.woggioni.gbcs.cli.impl
import net.woggioni.jwo.Application
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.Path
abstract class GbcsCommand : Runnable { abstract class GbcsCommand : Runnable {
@@ -8,4 +10,10 @@ abstract class GbcsCommand : Runnable {
@CommandLine.Option(names = ["-h", "--help"], usageHelp = true) @CommandLine.Option(names = ["-h", "--help"], usageHelp = true)
var usageHelp = false var usageHelp = false
private set private set
protected fun findConfigurationFile(app: Application, fileName : String): Path {
val confDir = app.computeConfigurationDirectory()
val configurationFile = confDir.resolve(fileName)
return configurationFile
}
} }

View File

@@ -0,0 +1,132 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.error
import net.woggioni.gbcs.common.info
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
import picocli.CommandLine
import java.security.SecureRandom
import java.time.Duration
import java.time.Instant
import java.util.Base64
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicLong
import kotlin.random.Random
@CommandLine.Command(
name = "benchmark",
description = ["Run a load test against the server"],
showDefaultValues = true
)
class BenchmarkCommand : GbcsCommand() {
private val log = contextLogger()
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@CommandLine.Option(
names = ["-e", "--entries"],
description = ["Total number of elements to be added to the cache"],
paramLabel = "NUMBER_OF_ENTRIES"
)
private var numberOfEntries = 1000
override fun run() {
val clientCommand = spec.parent().userObject() as ClientCommand
val profile = clientCommand.profileName.let { profileName ->
clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
}
val client = GbcsClient(profile)
val entryGenerator = sequence {
val random = Random(SecureRandom.getInstance("NativePRNGNonBlocking").nextLong())
while (true) {
val key = Base64.getUrlEncoder().encode(random.nextBytes(16)).toString(Charsets.UTF_8)
val value = random.nextBytes(0x1000)
yield(key to value)
}
}
val entries = let {
val completionQueue = LinkedBlockingQueue<Future<Pair<String, ByteArray>>>(numberOfEntries)
val start = Instant.now()
val totalElapsedTime = AtomicLong(0)
entryGenerator.take(numberOfEntries).forEach { entry ->
val requestStart = System.nanoTime()
val future = client.put(entry.first, entry.second).thenApply { entry }
future.whenComplete { _, _ ->
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
completionQueue.put(future)
}
}
val inserted = sequence<Pair<String, ByteArray>> {
var completionCounter = 0
while (completionCounter < numberOfEntries) {
val future = completionQueue.take()
try {
yield(future.get())
} catch (ee: ExecutionException) {
val cause = ee.cause ?: ee
log.error(cause.message, cause)
}
completionCounter += 1
}
}.toList()
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
"Insertion rate: ${numberOfEntries.toDouble() / elapsed * 1000} ops/s"
}
log.info {
"Average time per insertion: ${totalElapsedTime.get() / numberOfEntries.toDouble() * 1000} ms"
}
inserted
}
log.info {
"Inserted ${entries.size} entries"
}
if (entries.isNotEmpty()) {
val completionQueue = LinkedBlockingQueue<Future<Unit>>(entries.size)
val start = Instant.now()
val totalElapsedTime = AtomicLong(0)
entries.forEach { entry ->
val requestStart = System.nanoTime()
val future = client.get(entry.first).thenApply {
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
if (it == null) {
log.error {
"Missing entry for key '${entry.first}'"
}
} else if (!entry.second.contentEquals(it)) {
log.error {
"Retrieved a value different from what was inserted for key '${entry.first}'"
}
}
}
future.whenComplete { _, _ ->
completionQueue.put(future)
}
}
var completionCounter = 0
while (completionCounter < entries.size) {
completionQueue.take()
completionCounter += 1
}
val end = Instant.now()
log.info {
val elapsed = Duration.between(start, end).toMillis()
"Retrieval rate: ${entries.size.toDouble() / elapsed * 1000} ops/s"
}
log.info {
"Average time per retrieval: ${totalElapsedTime.get() / numberOfEntries.toDouble() * 1e6} ms"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}
}
}

View File

@@ -0,0 +1,41 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
import net.woggioni.jwo.Application
import picocli.CommandLine
import java.nio.file.Path
@CommandLine.Command(
name = "client",
description = ["GBCS client"],
showDefaultValues = true
)
class ClientCommand(app : Application) : GbcsCommand() {
@CommandLine.Option(
names = ["-c", "--configuration"],
description = ["Path to the client configuration file"],
paramLabel = "CONFIGURATION_FILE"
)
private var configurationFile : Path = findConfigurationFile(app, "gbcs-client.xml")
@CommandLine.Option(
names = ["-p", "--profile"],
description = ["Name of the client profile to be used"],
paramLabel = "PROFILE",
required = true
)
var profileName : String? = null
val configuration : GbcsClient.Configuration by lazy {
GbcsClient.Configuration.parse(configurationFile)
}
override fun run() {
println("Available profiles:")
configuration.profiles.forEach { (profileName, _) ->
println(profileName)
}
}
}

View File

@@ -0,0 +1,51 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
import picocli.CommandLine
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command(
name = "get",
description = ["Fetch a value from the cache with the specified key"],
showDefaultValues = true
)
class GetCommand : GbcsCommand() {
private val log = contextLogger()
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@CommandLine.Option(
names = ["-k", "--key"],
description = ["The key for the new value"],
paramLabel = "KEY"
)
private var key : String = ""
@CommandLine.Option(
names = ["-v", "--value"],
description = ["Path to a file where the retrieved value will be written (defaults to stdout)"],
paramLabel = "VALUE_FILE",
)
private var output : Path? = null
override fun run() {
val clientCommand = spec.parent().userObject() as ClientCommand
val profile = clientCommand.profileName.let { profileName ->
clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
}
GbcsClient(profile).use { client ->
client.get(key).thenApply { value ->
value?.let {
(output?.let(Files::newOutputStream) ?: System.out).use {
it.write(value)
}
} ?: throw NoSuchElementException("No value found for key $key")
}.get()
}
}
}

View File

@@ -1,6 +1,6 @@
package net.woggioni.gbcs.cli.impl.commands package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import net.woggioni.gbcs.cli.impl.GbcsCommand import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.jwo.UncloseableOutputStream import net.woggioni.jwo.UncloseableOutputStream

View File

@@ -0,0 +1,48 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.InputStreamConverter
import net.woggioni.gbcs.client.GbcsClient
import picocli.CommandLine
import java.io.InputStream
@CommandLine.Command(
name = "put",
description = ["Add or replace a value to the cache with the specified key"],
showDefaultValues = true
)
class PutCommand : GbcsCommand() {
private val log = contextLogger()
@CommandLine.Spec
private lateinit var spec: CommandLine.Model.CommandSpec
@CommandLine.Option(
names = ["-k", "--key"],
description = ["The key for the new value"],
paramLabel = "KEY"
)
private var key : String = ""
@CommandLine.Option(
names = ["-v", "--value"],
description = ["Path to a file containing the value to be added (defaults to stdin)"],
paramLabel = "VALUE_FILE",
converter = [InputStreamConverter::class]
)
private var value : InputStream = System.`in`
override fun run() {
val clientCommand = spec.parent().userObject() as ClientCommand
val profile = clientCommand.profileName.let { profileName ->
clientCommand.configuration.profiles[profileName]
?: throw IllegalArgumentException("Profile $profileName does not exist in configuration")
}
GbcsClient(profile).use { client ->
value.use {
client.put(key, it.readAllBytes())
}.get()
}
}
}

View File

@@ -0,0 +1,67 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.server.GradleBuildCacheServer
import net.woggioni.gbcs.server.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.debug
import net.woggioni.gbcs.common.info
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import picocli.CommandLine
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command(
name = "server",
description = ["GBCS server"],
showDefaultValues = true
)
class ServerCommand(app : Application) : GbcsCommand() {
private val log = contextLogger()
private fun createDefaultConfigurationFile(configurationFile: Path) {
log.info {
"Creating default configuration file at '$configurationFile'"
}
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
Files.newOutputStream(configurationFile).use { outputStream ->
defaultConfigurationFileResource.openStream().use { inputStream ->
JWO.copy(inputStream, outputStream)
}
}
}
@CommandLine.Option(
names = ["-c", "--config-file"],
description = ["Read the application configuration from this file"],
paramLabel = "CONFIG_FILE"
)
private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml")
val configuration : Configuration by lazy {
GradleBuildCacheServer.loadConfiguration(configurationFile)
}
override fun run() {
if (!Files.exists(configurationFile)) {
Files.createDirectories(configurationFile.parent)
createDefaultConfigurationFile(configurationFile)
}
val configuration = GradleBuildCacheServer.loadConfiguration(configurationFile)
log.debug {
ByteArrayOutputStream().also {
GradleBuildCacheServer.dumpConfiguration(configuration, it)
}.let {
"Server configuration:\n${String(it.toByteArray())}"
}
}
val server = GradleBuildCacheServer(configuration)
server.run().use {
}
}
}

View File

@@ -0,0 +1,13 @@
package net.woggioni.gbcs.cli.impl.converters
import picocli.CommandLine
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
class InputStreamConverter : CommandLine.ITypeConverter<InputStream> {
override fun convert(value: String): InputStream {
return Files.newInputStream(Paths.get(value))
}
}

View File

@@ -15,8 +15,6 @@
<root level="info"> <root level="info">
<appender-ref ref="console"/> <appender-ref ref="console"/>
</root> </root>
<logger name="io.netty" level="debug"/>
<logger name="io.netty.handler.ssl.BouncyCastlePemReader" level="info"/>
<logger name="com.google.code.yanf4j" level="warn"/> <logger name="com.google.code.yanf4j" level="warn"/>
<logger name="net.rubyeye.xmemcached" level="warn"/> <logger name="net.rubyeye.xmemcached" level="warn"/>
</configuration> </configuration>

15
gbcs-client/build.gradle Normal file
View File

@@ -0,0 +1,15 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
}
dependencies {
implementation project(':gbcs-api')
implementation project(':gbcs-common')
implementation catalog.picocli
implementation catalog.slf4j.api
implementation catalog.netty.buffer
implementation catalog.netty.codec.http
}

View File

@@ -0,0 +1,17 @@
module net.woggioni.gbcs.client {
requires io.netty.handler;
requires io.netty.codec.http;
requires io.netty.transport;
requires kotlin.stdlib;
requires io.netty.common;
requires io.netty.buffer;
requires java.xml;
requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.api;
requires io.netty.codec;
requires org.slf4j;
exports net.woggioni.gbcs.client;
opens net.woggioni.gbcs.client.schema;
}

View File

@@ -0,0 +1,253 @@
package net.woggioni.gbcs.client
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelOption
import io.netty.channel.ChannelPipeline
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.pool.AbstractChannelPoolHandler
import io.netty.channel.pool.ChannelPool
import io.netty.channel.pool.FixedChannelPool
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.DecoderException
import io.netty.handler.codec.http.DefaultFullHttpRequest
import io.netty.handler.codec.http.FullHttpRequest
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.HttpClientCodec
import io.netty.handler.codec.http.HttpContentDecompressor
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpObjectAggregator
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.Future
import io.netty.util.concurrent.GenericFutureListener
import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.common.debug
import net.woggioni.gbcs.client.impl.Parser
import java.net.InetSocketAddress
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import io.netty.util.concurrent.Future as NettyFuture
class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable {
private val group: NioEventLoopGroup
private var sslContext: SslContext
private val log = contextLogger()
private val pool: ChannelPool
data class Configuration(
val profiles: Map<String, Profile>
) {
sealed class Authentication {
data class TlsClientAuthenticationCredentials(
val key: PrivateKey,
val certificateChain: Array<X509Certificate>
) : Authentication()
data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication()
}
data class Profile(
val serverURI: URI,
val authentication: Authentication?,
val maxConnections : Int
)
companion object {
fun parse(path: Path): Configuration {
return Files.newInputStream(path).use {
Xml.parseXml(path.toUri().toURL(), it)
}.let(Parser::parse)
}
}
}
init {
group = NioEventLoopGroup()
sslContext = SslContextBuilder.forClient().also { builder ->
(profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials ->
builder.keyManager(
tlsClientAuthenticationCredentials.key,
*tlsClientAuthenticationCredentials.certificateChain
)
}
}.build()
val (scheme, host, port) = profile.serverURI.run {
Triple(
if (scheme == null) "http" else profile.serverURI.scheme,
host,
port.takeIf { it > 0 } ?: if ("https" == scheme.lowercase()) 443 else 80
)
}
val bootstrap = Bootstrap().apply {
group(group)
channel(NioSocketChannel::class.java)
option(ChannelOption.TCP_NODELAY, true)
option(ChannelOption.SO_KEEPALIVE, true)
remoteAddress(InetSocketAddress(host, port))
}
val channelPoolHandler = object : AbstractChannelPoolHandler() {
@Volatile
private var connectionCount = AtomicInteger()
@Volatile
private var leaseCount = AtomicInteger()
override fun channelReleased(ch: Channel) {
log.debug {
"Released lease ${leaseCount.decrementAndGet()}"
}
}
override fun channelAcquired(ch: Channel?) {
log.debug {
"Acquired lease ${leaseCount.getAndIncrement()}"
}
}
override fun channelCreated(ch: Channel) {
log.debug {
"Created connection ${connectionCount.getAndIncrement()}"
}
val pipeline: ChannelPipeline = ch.pipeline()
// Add SSL handler if needed
if ("https".equals(scheme, ignoreCase = true)) {
pipeline.addLast("ssl", sslContext.newHandler(ch.alloc(), host, port))
}
// HTTP handlers
pipeline.addLast("codec", HttpClientCodec())
pipeline.addLast("decompressor", HttpContentDecompressor())
pipeline.addLast("aggregator", HttpObjectAggregator(1048576))
pipeline.addLast("chunked", ChunkedWriteHandler())
}
}
pool = FixedChannelPool(bootstrap, channelPoolHandler, profile.maxConnections)
}
fun get(key: String): CompletableFuture<ByteArray?> {
return sendRequest(profile.serverURI.resolve(key), HttpMethod.GET, null)
.thenApply {
val status = it.status()
if (it.status() == HttpResponseStatus.NOT_FOUND) {
null
} else if (it.status() != HttpResponseStatus.OK) {
throw HttpException(status)
} else {
it.content()
}
}.thenApply { maybeByteBuf ->
maybeByteBuf?.let {
val result = ByteArray(it.readableBytes())
it.getBytes(0, result)
result
}
}
}
fun put(key: String, content: ByteArray): CompletableFuture<Unit> {
return sendRequest(profile.serverURI.resolve(key), HttpMethod.PUT, content).thenApply {
val status = it.status()
if (it.status() != HttpResponseStatus.CREATED) {
throw HttpException(status)
}
}
}
private fun sendRequest(uri: URI, method: HttpMethod, body: ByteArray?): CompletableFuture<FullHttpResponse> {
val responseFuture = CompletableFuture<FullHttpResponse>()
// Custom handler for processing responses
pool.acquire().addListener(object : GenericFutureListener<NettyFuture<Channel>> {
override fun operationComplete(channelFuture: Future<Channel>) {
if (channelFuture.isSuccess) {
val channel = channelFuture.now
val pipeline = channel.pipeline()
channel.pipeline().addLast("handler", object : SimpleChannelInboundHandler<FullHttpResponse>() {
override fun channelRead0(
ctx: ChannelHandlerContext,
response: FullHttpResponse
) {
responseFuture.complete(response)
pipeline.removeLast()
pool.release(channel)
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
val ex = when (cause) {
is DecoderException -> cause.cause
else -> cause
}
responseFuture.completeExceptionally(ex)
ctx.close()
pipeline.removeLast()
pool.release(channel)
}
})
// Prepare the HTTP request
val request: FullHttpRequest = let {
val content: ByteBuf? = body?.takeIf(ByteArray::isNotEmpty)?.let(Unpooled::wrappedBuffer)
DefaultFullHttpRequest(
HttpVersion.HTTP_1_1,
method,
uri.rawPath,
content ?: Unpooled.buffer(0)
).apply {
headers().apply {
if (content != null) {
set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM)
set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes())
}
set(HttpHeaderNames.HOST, profile.serverURI.host)
set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
set(
HttpHeaderNames.ACCEPT_ENCODING,
HttpHeaderValues.GZIP.toString() + "," + HttpHeaderValues.DEFLATE.toString()
)
// Add basic auth if configured
(profile.authentication as? Configuration.Authentication.BasicAuthenticationCredentials)?.let { credentials ->
val auth = "${credentials.username}:${credentials.password}"
val encodedAuth = Base64.getEncoder().encodeToString(auth.toByteArray())
set(HttpHeaderNames.AUTHORIZATION, "Basic $encodedAuth")
}
}
}
}
// Set headers
// Send the request
channel.writeAndFlush(request)
}
}
})
return responseFuture
}
fun shutDown(): NettyFuture<*> {
return group.shutdownGracefully()
}
override fun close() {
shutDown().sync()
}
}

View File

@@ -0,0 +1,9 @@
package net.woggioni.gbcs.client
import io.netty.handler.codec.http.HttpResponseStatus
class HttpException(private val status : HttpResponseStatus) : RuntimeException(status.reasonPhrase()) {
override val message: String
get() = "Http status ${status.code()}: ${status.reasonPhrase()}"
}

View File

@@ -0,0 +1,69 @@
package net.woggioni.gbcs.client.impl
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import net.woggioni.gbcs.client.GbcsClient
import org.w3c.dom.Document
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
object Parser {
fun parse(document: Document): GbcsClient.Configuration {
val root = document.documentElement
val profiles = mutableMapOf<String, GbcsClient.Configuration.Profile>()
for (child in root.asIterable()) {
val tagName = child.localName
when (tagName) {
"profile" -> {
val name = child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI) ?: throw ConfigurationException("base-url attribute is required")
var authentication: GbcsClient.Configuration.Authentication? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"tls-client-auth" -> {
val keyStoreFile = gchild.renderAttribute("key-store-file")
val keyStorePassword =
gchild.renderAttribute("key-store-password")
val keyAlias = gchild.renderAttribute("key-alias")
val keyPassword = gchild.renderAttribute("key-password")
val keystore = KeyStore.getInstance("PKCS12").apply {
Files.newInputStream(Path.of(keyStoreFile)).use {
load(it, keyStorePassword?.toCharArray())
}
}
val key = keystore.getKey(keyAlias, keyPassword?.toCharArray()) as PrivateKey
val certChain = keystore.getCertificateChain(keyAlias).asSequence()
.map { it as X509Certificate }
.toList()
.toTypedArray()
authentication =
GbcsClient.Configuration.Authentication.TlsClientAuthenticationCredentials(key, certChain)
}
"basic-auth" -> {
val username = gchild.renderAttribute("user") ?: throw ConfigurationException("username attribute is required")
val password = gchild.renderAttribute("password") ?: throw ConfigurationException("password attribute is required")
authentication =
GbcsClient.Configuration.Authentication.BasicAuthenticationCredentials(username, password)
}
}
}
val maxConnections = child.renderAttribute("max-connections")
?.let(String::toInt)
?: 50
profiles[name] = GbcsClient.Configuration.Profile(uri, authentication, maxConnections)
}
}
}
return GbcsClient.Configuration(profiles)
}
}

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs.client"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
elementFormDefault="unqualified"
>
<xs:element name="profiles" type="gbcs-client:profilesType"/>
<xs:complexType name="profilesType">
<xs:sequence minOccurs="0">
<xs:element name="profile" type="gbcs-client:profileType" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="profileType">
<xs:choice>
<xs:element name="basic-auth" type="gbcs-client:basicAuthType"/>
<xs:element name="tls-client-auth" type="gbcs-client:tlsClientAuthType"/>
</xs:choice>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="base-url" type="xs:anyURI" use="required"/>
<xs:attribute name="max-connections" type="xs:positiveInteger" default="50"/>
</xs:complexType>
<xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
<xs:attribute name="key-alias" type="xs:token" use="required"/>
<xs:attribute name="key-password" type="xs:string" use="optional"/>
</xs:complexType>
</xs:schema>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs-client:profiles xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs-client="urn:net.woggioni.gbcs.client"
xs:schemaLocation="urn:net.woggioni.gbcs.client jms://net.woggioni.gbcs.client/net/woggioni/gbcs/client/schema/gbcs-client.xsd"
>
<profile name="profile1" base-url="https://gbcs1.example.com/">
<tls-client-auth
key-store-file="keystore.pfx"
key-store-password="password"
key-alias="woggioni@c962475fa38"
key-password="key-password"/>
</profile>
<profile name="profile2" base-url="https://gbcs2.example.com/">
<basic-auth user="user" password="password"/>
</profile>
</gbcs-client:profiles>

View File

@@ -6,8 +6,9 @@ plugins {
} }
dependencies { dependencies {
compileOnly project(':gbcs-api') implementation project(':gbcs-api')
compileOnly catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.jwo
} }
publishing { publishing {

View File

@@ -1,8 +1,9 @@
module net.woggioni.gbcs.base { module net.woggioni.gbcs.common {
requires java.xml; requires java.xml;
requires java.logging; requires java.logging;
requires org.slf4j; requires org.slf4j;
requires kotlin.stdlib; requires kotlin.stdlib;
requires net.woggioni.jwo;
exports net.woggioni.gbcs.base; exports net.woggioni.gbcs.common;
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.base package net.woggioni.gbcs.common
import java.net.URI import java.net.URI
import java.net.URL import java.net.URL
@@ -6,7 +6,7 @@ import java.net.URL
object GBCS { object GBCS {
fun String.toUrl() : URL = URL.of(URI(this), null) fun String.toUrl() : URL = URL.of(URI(this), null)
const val GBCS_NAMESPACE_URI: String = "urn:net.woggioni.gbcs" const val GBCS_NAMESPACE_URI: String = "urn:net.woggioni.gbcs.server"
const val GBCS_PREFIX: String = "gbcs" const val GBCS_PREFIX: String = "gbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance" const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.base package net.woggioni.gbcs.common
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.base package net.woggioni.gbcs.common
data class HostAndPort(val host: String, val port: Int = 0) { data class HostAndPort(val host: String, val port: Int = 0) {

View File

@@ -1,7 +1,8 @@
package net.woggioni.gbcs.base package net.woggioni.gbcs.common
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.logging.LogManager import java.util.logging.LogManager
@@ -52,6 +53,12 @@ inline fun log(log : Logger,
} }
} }
inline fun Logger.log(level : Level, messageBuilder : () -> String) {
if(isEnabledForLevel(level)) {
makeLoggingEventBuilder(level).log(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder : () -> String) { inline fun Logger.trace(messageBuilder : () -> String) {
if(isTraceEnabled) { if(isTraceEnabled) {
trace(messageBuilder()) trace(messageBuilder())

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.base package net.woggioni.gbcs.common
import java.security.SecureRandom import java.security.SecureRandom
import java.security.spec.KeySpec import java.security.spec.KeySpec

View File

@@ -1,6 +1,8 @@
package net.woggioni.gbcs.base package net.woggioni.gbcs.common
import net.woggioni.jwo.JWO
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.Node import org.w3c.dom.Node
@@ -80,31 +82,36 @@ class Xml(val doc: Document, val element: Element) {
private val log = LoggerFactory.getLogger(ErrorHandler::class.java) private val log = LoggerFactory.getLogger(ErrorHandler::class.java)
} }
override fun warning(ex: SAXParseException) { override fun warning(ex: SAXParseException)= err(ex, Level.WARN)
log.warn(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
}
override fun error(ex: SAXParseException) { private fun err(ex: SAXParseException, level: Level) {
log.error( log.log(level) {
"Problem at {}:{}:{} parsing deployment configuration: {}", "Problem at ${fileURL}:${ex.lineNumber}:${ex.columnNumber} parsing deployment configuration: ${ex.message}"
fileURL, ex.lineNumber, ex.columnNumber, ex.message }
)
throw ex throw ex
} }
override fun fatalError(ex: SAXParseException) { override fun error(ex: SAXParseException) = err(ex, Level.ERROR)
log.error( override fun fatalError(ex: SAXParseException) = err(ex, Level.ERROR)
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
throw ex
}
} }
companion object { companion object {
private val dictMap: Map<String, Map<String, Any>> = sequenceOf(
"env" to System.getenv().asSequence().map { (k, v) -> k to (v as Any) }.toMap(),
"sys" to System.getProperties().asSequence().map { (k, v) -> k as String to (v as Any) }.toMap()
).toMap()
private fun renderConfigurationTemplate(template: String): String {
return JWO.renderTemplate(template, emptyMap(), dictMap).replace("$$", "$")
}
fun Element.renderAttribute(name : String, namespaceURI: String? = null) = if(namespaceURI == null) {
getAttribute(name)
} else {
getAttributeNS(name, namespaceURI)
}.takeIf(String::isNotEmpty)?.let(Companion::renderConfigurationTemplate)
fun Element.asIterable() = Iterable { ElementIterator(this, null) } fun Element.asIterable() = Iterable { ElementIterator(this, null) }
fun NodeList.asIterable() = Iterable { NodeListIterator(this) } fun NodeList.asIterable() = Iterable { NodeListIterator(this) }

View File

@@ -1,14 +0,0 @@
import net.woggioni.gbcs.api.CacheProvider;
module net.woggioni.gbcs.memcached {
requires net.woggioni.gbcs.base;
requires net.woggioni.gbcs.api;
requires com.googlecode.xmemcached;
requires net.woggioni.jwo;
requires java.xml;
requires kotlin.stdlib;
provides CacheProvider with net.woggioni.gbcs.memcached.MemcachedCacheProvider;
opens net.woggioni.gbcs.memcached.schema;
}

View File

@@ -1 +0,0 @@
net.woggioni.gbcs.memcached.MemcachedCacheProvider

View File

@@ -1,6 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id 'java-library' id 'java-library'
id 'maven-publish' id 'maven-publish'
@@ -32,7 +29,7 @@ configurations {
} }
dependencies { dependencies {
compileOnly project(':gbcs-base') compileOnly project(':gbcs-common')
compileOnly project(':gbcs-api') compileOnly project(':gbcs-api')
compileOnly catalog.jwo compileOnly catalog.jwo
implementation catalog.xmemcached implementation catalog.xmemcached

View File

@@ -0,0 +1,14 @@
import net.woggioni.gbcs.api.CacheProvider;
module net.woggioni.gbcs.server.memcached {
requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.api;
requires com.googlecode.xmemcached;
requires net.woggioni.jwo;
requires java.xml;
requires kotlin.stdlib;
provides CacheProvider with net.woggioni.gbcs.server.memcached.MemcachedCacheProvider;
opens net.woggioni.gbcs.server.memcached.schema;
}

View File

@@ -1,13 +1,12 @@
package net.woggioni.gbcs.memcached package net.woggioni.gbcs.server.memcached
import net.rubyeye.xmemcached.MemcachedClient
import net.rubyeye.xmemcached.XMemcachedClientBuilder import net.rubyeye.xmemcached.XMemcachedClientBuilder
import net.rubyeye.xmemcached.command.BinaryCommandFactory import net.rubyeye.xmemcached.command.BinaryCommandFactory
import net.rubyeye.xmemcached.transcoders.CompressionMode import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.rubyeye.xmemcached.transcoders.SerializingTranscoder import net.rubyeye.xmemcached.transcoders.SerializingTranscoder
import net.woggioni.gbcs.api.Cache import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.api.exception.ContentTooLargeException import net.woggioni.gbcs.api.exception.ContentTooLargeException
import net.woggioni.gbcs.base.HostAndPort import net.woggioni.gbcs.common.HostAndPort
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress

View File

@@ -1,8 +1,8 @@
package net.woggioni.gbcs.memcached package net.woggioni.gbcs.server.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.HostAndPort import net.woggioni.gbcs.common.HostAndPort
import java.time.Duration import java.time.Duration
data class MemcachedCacheConfiguration( data class MemcachedCacheConfiguration(
@@ -20,7 +20,7 @@ data class MemcachedCacheConfiguration(
compressionMode compressionMode
) )
override fun getNamespaceURI() = "urn:net.woggioni.gbcs-memcached" override fun getNamespaceURI() = "urn:net.woggioni.gbcs.server.memcached"
override fun getTypeName() = "memcachedCacheType" override fun getTypeName() = "memcachedCacheType"
} }

View File

@@ -1,41 +1,36 @@
package net.woggioni.gbcs.memcached package net.woggioni.gbcs.server.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.base.GBCS import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.base.HostAndPort import net.woggioni.gbcs.common.GBCS
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.HostAndPort
import net.woggioni.gbcs.base.Xml.Companion.asIterable import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.time.Duration import java.time.Duration
class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> { class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd" override fun getXmlSchemaLocation() = "jpms://net.woggioni.gbcs.server.memcached/net/woggioni/gbcs/server/memcached/schema/gbcs-memcached.xsd"
override fun getXmlType() = "memcachedCacheType" override fun getXmlType() = "memcachedCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.gbcs-memcached" override fun getXmlNamespace() = "urn:net.woggioni.gbcs.server.memcached"
val xmlNamespacePrefix : String val xmlNamespacePrefix : String
get() = "gbcs-memcached" get() = "gbcs-memcached"
override fun deserialize(el: Element): MemcachedCacheConfiguration { override fun deserialize(el: Element): MemcachedCacheConfiguration {
val servers = mutableListOf<HostAndPort>() val servers = mutableListOf<HostAndPort>()
val maxAge = el.getAttribute("max-age") val maxAge = el.renderAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse) ?.let(Duration::parse)
?: Duration.ofDays(1) ?: Duration.ofDays(1)
val maxSize = el.getAttribute("max-size") val maxSize = el.renderAttribute("max-size")
.takeIf(String::isNotEmpty)
?.let(String::toInt) ?.let(String::toInt)
?: 0x100000 ?: 0x100000
val enableCompression = el.getAttribute("enable-compression") val compressionMode = el.renderAttribute("compression-mode")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: false
val compressionMode = el.getAttribute("compression-mode")
.takeIf(String::isNotEmpty)
?.let { ?.let {
when (it) { when (it) {
"gzip" -> CompressionMode.GZIP "gzip" -> CompressionMode.GZIP
@@ -44,11 +39,13 @@ class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
} }
} }
?: CompressionMode.ZIP ?: CompressionMode.ZIP
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) val digestAlgorithm = el.renderAttribute("digest")
for (child in el.asIterable()) { for (child in el.asIterable()) {
when (child.nodeName) { when (child.nodeName) {
"server" -> { "server" -> {
servers.add(HostAndPort(child.getAttribute("host"), child.getAttribute("port").toInt())) val host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
val port = child.renderAttribute("port")?.toInt() ?: throw ConfigurationException("port attribute is required")
servers.add(HostAndPort(host, port))
} }
} }
} }

View File

@@ -0,0 +1 @@
net.woggioni.gbcs.server.memcached.MemcachedCacheProvider

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs-memcached" <xs:schema targetNamespace="urn:net.woggioni.gbcs.server.memcached"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached" xmlns:gbcs-memcached="urn:net.woggioni.gbcs.server.memcached"
xmlns:gbcs="urn:net.woggioni.gbcs" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema"> xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="classpath:net/woggioni/gbcs/schema/gbcs.xsd" namespace="urn:net.woggioni.gbcs"/> <xs:import schemaLocation="jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd" namespace="urn:net.woggioni.gbcs.server"/>
<xs:complexType name="memcachedServerType"> <xs:complexType name="memcachedServerType">
<xs:attribute name="host" type="xs:string" use="required"/> <xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:positiveInteger" use="required"/> <xs:attribute name="port" type="xs:positiveInteger" use="required"/>
</xs:complexType> </xs:complexType>

35
gbcs-server/build.gradle Normal file
View File

@@ -0,0 +1,35 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
id 'maven-publish'
}
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.netty.codec.http
api project(':gbcs-common')
api project(':gbcs-api')
// runtimeOnly catalog.slf4j.jdk14
testRuntimeOnly catalog.logback.classic
testImplementation catalog.bcprov.jdk18on
testImplementation catalog.bcpkix.jdk18on
testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine
testRuntimeOnly project(":gbcs-server-memcached")
}
publishing {
publications {
maven(MavenPublication) {
from(components["java"])
}
}
}

View File

@@ -1,7 +1,7 @@
import net.woggioni.gbcs.api.CacheProvider; import net.woggioni.gbcs.api.CacheProvider;
import net.woggioni.gbcs.cache.FileSystemCacheProvider; import net.woggioni.gbcs.server.cache.FileSystemCacheProvider;
module net.woggioni.gbcs { module net.woggioni.gbcs.server {
requires java.sql; requires java.sql;
requires java.xml; requires java.xml;
requires java.logging; requires java.logging;
@@ -15,13 +15,13 @@ module net.woggioni.gbcs {
requires io.netty.codec; requires io.netty.codec;
requires org.slf4j; requires org.slf4j;
requires net.woggioni.jwo; requires net.woggioni.jwo;
requires net.woggioni.gbcs.base; requires net.woggioni.gbcs.common;
requires net.woggioni.gbcs.api; requires net.woggioni.gbcs.api;
exports net.woggioni.gbcs; exports net.woggioni.gbcs.server;
opens net.woggioni.gbcs; opens net.woggioni.gbcs.server;
opens net.woggioni.gbcs.schema; opens net.woggioni.gbcs.server.schema;
uses CacheProvider; uses CacheProvider;
provides CacheProvider with FileSystemCacheProvider; provides CacheProvider with FileSystemCacheProvider;

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs package net.woggioni.gbcs.server
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
@@ -45,18 +45,18 @@ import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.api.exception.ContentTooLargeException import net.woggioni.gbcs.api.exception.ContentTooLargeException
import net.woggioni.gbcs.auth.AbstractNettyHttpAuthenticator import net.woggioni.gbcs.server.auth.AbstractNettyHttpAuthenticator
import net.woggioni.gbcs.auth.Authorizer import net.woggioni.gbcs.server.auth.Authorizer
import net.woggioni.gbcs.auth.ClientCertificateValidator import net.woggioni.gbcs.server.auth.ClientCertificateValidator
import net.woggioni.gbcs.auth.RoleAuthorizer import net.woggioni.gbcs.server.auth.RoleAuthorizer
import net.woggioni.gbcs.base.GBCS.toUrl import net.woggioni.gbcs.common.GBCS.toUrl
import net.woggioni.gbcs.base.PasswordSecurity.decodePasswordHash import net.woggioni.gbcs.common.PasswordSecurity.decodePasswordHash
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.base.contextLogger import net.woggioni.gbcs.common.contextLogger
import net.woggioni.gbcs.base.info import net.woggioni.gbcs.common.info
import net.woggioni.gbcs.configuration.Parser import net.woggioni.gbcs.server.configuration.Parser
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2 import net.woggioni.jwo.Tuple2
import java.io.OutputStream import java.io.OutputStream
@@ -507,9 +507,9 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() } val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
fun loadConfiguration(configurationFile: Path): Configuration { fun loadConfiguration(configurationFile: Path): Configuration {
val dbf = Xml.newDocumentBuilderFactory(null) val doc = Files.newInputStream(configurationFile).use {
val db = dbf.newDocumentBuilder() Xml.parseXml(configurationFile.toUri().toURL(), it)
val doc = Files.newInputStream(configurationFile).use(db::parse) }
return Parser.parse(doc) return Parser.parse(doc)
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs package net.woggioni.gbcs.server
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import org.slf4j.Logger import org.slf4j.Logger

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.auth package net.woggioni.gbcs.server.auth
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.auth package net.woggioni.gbcs.server.auth
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.auth package net.woggioni.gbcs.server.auth
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.channel.ChannelInboundHandlerAdapter

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.auth package net.woggioni.gbcs.server.auth
import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.cache package net.woggioni.gbcs.server.cache
import net.woggioni.gbcs.api.Cache import net.woggioni.gbcs.api.Cache
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO

View File

@@ -1,7 +1,7 @@
package net.woggioni.gbcs.cache package net.woggioni.gbcs.server.cache
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.GBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.jwo.Application import net.woggioni.jwo.Application
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration

View File

@@ -1,8 +1,9 @@
package net.woggioni.gbcs.cache package net.woggioni.gbcs.server.cache
import net.woggioni.gbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.base.GBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import java.nio.file.Path import java.nio.file.Path
@@ -11,29 +12,25 @@ import java.util.zip.Deflater
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> { class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/schema/gbcs.xsd" override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/server/schema/gbcs.xsd"
override fun getXmlType() = "fileSystemCacheType" override fun getXmlType() = "fileSystemCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.gbcs" override fun getXmlNamespace() = "urn:net.woggioni.gbcs.server"
override fun deserialize(el: Element): FileSystemCacheConfiguration { override fun deserialize(el: Element): FileSystemCacheConfiguration {
val path = el.getAttribute("path") val path = el.renderAttribute("path")
.takeIf(String::isNotEmpty)
?.let(Path::of) ?.let(Path::of)
val maxAge = el.getAttribute("max-age") val maxAge = el.renderAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse) ?.let(Duration::parse)
?: Duration.ofDays(1) ?: Duration.ofDays(1)
val enableCompression = el.getAttribute("enable-compression") val enableCompression = el.renderAttribute("enable-compression")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?.let(String::toBoolean)
?: true ?: true
val compressionLevel = el.getAttribute("compression-level") val compressionLevel = el.renderAttribute("compression-level")
.takeIf(String::isNotEmpty)
?.let(String::toInt) ?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION ?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) ?: "MD5" val digestAlgorithm = el.renderAttribute("digest") ?: "MD5"
return FileSystemCacheConfiguration( return FileSystemCacheConfiguration(
path, path,

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.configuration package net.woggioni.gbcs.server.configuration
import net.woggioni.gbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.configuration package net.woggioni.gbcs.server.configuration
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Configuration.Authentication import net.woggioni.gbcs.api.Configuration.Authentication
@@ -12,15 +12,15 @@ import net.woggioni.gbcs.api.Configuration.TlsCertificateExtractor
import net.woggioni.gbcs.api.Configuration.TrustStore import net.woggioni.gbcs.api.Configuration.TrustStore
import net.woggioni.gbcs.api.Configuration.User import net.woggioni.gbcs.api.Configuration.User
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml.Companion.asIterable import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.common.Xml.Companion.asIterable
import net.woggioni.gbcs.common.Xml.Companion.renderAttribute
import org.w3c.dom.Document import org.w3c.dom.Document
import org.w3c.dom.Element import org.w3c.dom.Element
import org.w3c.dom.TypeInfo import org.w3c.dom.TypeInfo
import java.lang.IllegalArgumentException
import java.nio.file.Paths import java.nio.file.Paths
object Parser { object Parser {
fun parse(document: Document): Configuration { fun parse(document: Document): Configuration {
val root = document.documentElement val root = document.documentElement
val anonymousUser = User("", null, emptySet()) val anonymousUser = User("", null, emptySet())
@@ -30,9 +30,8 @@ object Parser {
var users : Map<String, User> = mapOf(anonymousUser.name to anonymousUser) var users : Map<String, User> = mapOf(anonymousUser.name to anonymousUser)
var groups = emptyMap<String, Group>() var groups = emptyMap<String, Group>()
var tls: Tls? = null var tls: Tls? = null
val serverPath = root.getAttribute("path") val serverPath = root.renderAttribute("path")
val useVirtualThread = root.getAttribute("useVirtualThreads") val useVirtualThread = root.renderAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: true ?.let(String::toBoolean) ?: true
var authentication: Authentication? = null var authentication: Authentication? = null
for (child in root.asIterable()) { for (child in root.asIterable()) {
@@ -55,8 +54,8 @@ object Parser {
} }
"bind" -> { "bind" -> {
host = child.getAttribute("host") host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
port = Integer.parseInt(child.getAttribute("port")) port = Integer.parseInt(child.renderAttribute("port"))
} }
"cache" -> { "cache" -> {
@@ -81,14 +80,14 @@ object Parser {
for (ggchild in gchild.asIterable()) { for (ggchild in gchild.asIterable()) {
when (ggchild.localName) { when (ggchild.localName) {
"group-extractor" -> { "group-extractor" -> {
val attrName = ggchild.getAttribute("attribute-name") val attrName = ggchild.renderAttribute("attribute-name")
val pattern = ggchild.getAttribute("pattern") val pattern = ggchild.renderAttribute("pattern")
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern) tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
} }
"user-extractor" -> { "user-extractor" -> {
val attrName = ggchild.getAttribute("attribute-name") val attrName = ggchild.renderAttribute("attribute-name")
val pattern = ggchild.getAttribute("pattern") val pattern = ggchild.renderAttribute("pattern")
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern) tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
} }
} }
@@ -100,20 +99,17 @@ object Parser {
} }
"tls" -> { "tls" -> {
val verifyClients = child.getAttribute("verify-clients") val verifyClients = child.renderAttribute("verify-clients")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false ?.let(String::toBoolean) ?: false
var keyStore: KeyStore? = null var keyStore: KeyStore? = null
var trustStore: TrustStore? = null var trustStore: TrustStore? = null
for (granChild in child.asIterable()) { for (granChild in child.asIterable()) {
when (granChild.localName) { when (granChild.localName) {
"keystore" -> { "keystore" -> {
val keyStoreFile = Paths.get(granChild.getAttribute("file")) val keyStoreFile = Paths.get(granChild.renderAttribute("file"))
val keyStorePassword = granChild.getAttribute("password") val keyStorePassword = granChild.renderAttribute("password")
.takeIf(String::isNotEmpty) val keyAlias = granChild.renderAttribute("key-alias")
val keyAlias = granChild.getAttribute("key-alias") val keyPassword = granChild.renderAttribute("key-password")
val keyPassword = granChild.getAttribute("key-password")
.takeIf(String::isNotEmpty)
keyStore = KeyStore( keyStore = KeyStore(
keyStoreFile, keyStoreFile,
keyStorePassword, keyStorePassword,
@@ -123,11 +119,9 @@ object Parser {
} }
"truststore" -> { "truststore" -> {
val trustStoreFile = Paths.get(granChild.getAttribute("file")) val trustStoreFile = Paths.get(granChild.renderAttribute("file"))
val trustStorePassword = granChild.getAttribute("password") val trustStorePassword = granChild.renderAttribute("password")
.takeIf(String::isNotEmpty) val checkCertificateStatus = granChild.renderAttribute("check-certificate-status")
val checkCertificateStatus = granChild.getAttribute("check-certificate-status")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?.let(String::toBoolean)
?: false ?: false
trustStore = TrustStore( trustStore = TrustStore(
@@ -154,15 +148,15 @@ object Parser {
}.toSet() }.toSet()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map { private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map {
it.getAttribute("ref") it.renderAttribute("ref")
}.toSet() }.toSet()
private fun parseUsers(root: Element): Sequence<User> { private fun parseUsers(root: Element): Sequence<User> {
return root.asIterable().asSequence().filter { return root.asIterable().asSequence().filter {
it.localName == "user" it.localName == "user"
}.map { el -> }.map { el ->
val username = el.getAttribute("name") val username = el.renderAttribute("name")
val password = el.getAttribute("password").takeIf(String::isNotEmpty) val password = el.renderAttribute("password")
User(username, password, emptySet()) User(username, password, emptySet())
} }
} }
@@ -173,7 +167,7 @@ object Parser {
val groups = root.asIterable().asSequence().filter { val groups = root.asIterable().asSequence().filter {
it.localName == "group" it.localName == "group"
}.map { el -> }.map { el ->
val groupName = el.getAttribute("name") val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required")
var roles = emptySet<Role>() var roles = emptySet<Role>()
for (child in el.asIterable()) { for (child in el.asIterable()) {
when (child.localName) { when (child.localName) {

View File

@@ -1,9 +1,9 @@
package net.woggioni.gbcs.configuration package net.woggioni.gbcs.server.configuration
import net.woggioni.gbcs.api.CacheProvider import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.GBCS import net.woggioni.gbcs.common.GBCS
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.Xml
import org.w3c.dom.Document import org.w3c.dom.Document
object Serializer { object Serializer {

View File

@@ -0,0 +1 @@
net.woggioni.gbcs.server.cache.FileSystemCacheProvider

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="8080"/> <bind host="127.0.0.1" port="8080"/>
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authentication> <authentication>

View File

@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs" <xs:schema targetNamespace="urn:net.woggioni.gbcs.server"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:gbcs="urn:net.woggioni.gbcs" xmlns:gbcs="urn:net.woggioni.gbcs.server"
elementFormDefault="unqualified" elementFormDefault="unqualified">
>
<xs:element name="server" type="gbcs:serverType"/> <xs:element name="server" type="gbcs:serverType"/>
<xs:complexType name="serverType"> <xs:complexType name="serverType">
@@ -28,7 +27,7 @@
</xs:complexType> </xs:complexType>
<xs:complexType name="bindType"> <xs:complexType name="bindType">
<xs:attribute name="host" type="xs:string" use="required"/> <xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/> <xs:attribute name="port" type="xs:unsignedShort" use="required"/>
</xs:complexType> </xs:complexType>
@@ -48,14 +47,14 @@
<xs:complexType name="tlsCertificateAuthorizationType"> <xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence> <xs:sequence>
<xs:element name="group-extractor" type="gbcs:X500NameExtractorType"/> <xs:element name="group-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
<xs:element name="user-extractor" type="gbcs:X500NameExtractorType"/> <xs:element name="user-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
</xs:sequence> </xs:sequence>
</xs:complexType> </xs:complexType>
<xs:complexType name="X500NameExtractorType"> <xs:complexType name="X500NameExtractorType">
<xs:attribute name="attribute-name" type="xs:string"/> <xs:attribute name="attribute-name" type="xs:token"/>
<xs:attribute name="pattern" type="xs:string"/> <xs:attribute name="pattern" type="xs:token"/>
</xs:complexType> </xs:complexType>
<xs:complexType name="authorizationType"> <xs:complexType name="authorizationType">
@@ -85,7 +84,7 @@
</xs:complexType> </xs:complexType>
<xs:complexType name="userType"> <xs:complexType name="userType">
<xs:attribute name="name" type="xs:string" use="required"/> <xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="optional"/> <xs:attribute name="password" type="xs:string" use="optional"/>
</xs:complexType> </xs:complexType>
@@ -105,11 +104,11 @@
</xs:element> </xs:element>
<xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/> <xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
</xs:sequence> </xs:sequence>
<xs:attribute name="name" type="xs:string"/> <xs:attribute name="name" type="xs:token"/>
</xs:complexType> </xs:complexType>
<xs:simpleType name="role" final="restriction" > <xs:simpleType name="role" final="restriction" >
<xs:restriction base="xs:string"> <xs:restriction base="xs:token">
<xs:enumeration value="READER" /> <xs:enumeration value="READER" />
<xs:enumeration value="WRITER" /> <xs:enumeration value="WRITER" />
</xs:restriction> </xs:restriction>

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.utils; package net.woggioni.gbcs.server.test.utils;
import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500Name;

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.utils; package net.woggioni.gbcs.server.test.utils;
import net.woggioni.jwo.JWO; import net.woggioni.jwo.JWO;

View File

@@ -1,11 +1,11 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.gbcs.utils.NetworkUtils import net.woggioni.gbcs.server.test.utils.NetworkUtils
import java.net.URI import java.net.URI
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets

View File

@@ -1,6 +1,6 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import net.woggioni.gbcs.GradleBuildCacheServer import net.woggioni.gbcs.server.GradleBuildCacheServer
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll

View File

@@ -1,22 +1,17 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.gbcs.utils.CertificateUtils import net.woggioni.gbcs.server.test.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials import net.woggioni.gbcs.server.test.utils.CertificateUtils.X509Credentials
import net.woggioni.gbcs.utils.NetworkUtils import net.woggioni.gbcs.server.test.utils.NetworkUtils
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path

View File

@@ -1,9 +1,9 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,10 +1,10 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import net.woggioni.gbcs.base.GBCS.toUrl import net.woggioni.gbcs.common.GBCS.toUrl
import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory import net.woggioni.gbcs.common.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.configuration.Parser import net.woggioni.gbcs.server.configuration.Parser
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.server.configuration.Serializer
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
@@ -16,9 +16,9 @@ class ConfigurationTest {
@ValueSource( @ValueSource(
strings = [ strings = [
"classpath:net/woggioni/gbcs/test/gbcs-default.xml", "classpath:net/woggioni/gbcs/server/test/gbcs-default.xml",
"classpath:net/woggioni/gbcs/test/gbcs-memcached.xml", "classpath:net/woggioni/gbcs/server/test/gbcs-memcached.xml",
"classpath:net/woggioni/gbcs/test/gbcs-tls.xml", "classpath:net/woggioni/gbcs/server/test/gbcs-tls.xml",
] ]
) )
@ParameterizedTest @ParameterizedTest

View File

@@ -1,9 +1,8 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.common.PasswordSecurity.hashPassword
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration

View File

@@ -1,15 +1,14 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.common.Xml
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.server.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.utils.NetworkUtils import net.woggioni.gbcs.server.configuration.Serializer
import net.woggioni.gbcs.server.test.utils.NetworkUtils
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest

View File

@@ -1,35 +1,15 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials
import net.woggioni.gbcs.utils.NetworkUtils
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.ServerSocket
import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.KeyStore.PasswordProtection
import java.time.Duration
import java.util.Base64
import java.util.zip.Deflater
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import kotlin.random.Random
class TlsServerTest : AbstractTlsServerTest() { class TlsServerTest : AbstractTlsServerTest() {

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.server.test
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authentication> <authentication>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached" xmlns:gbcs-memcached="urn:net.woggioni.gbcs.server.memcached"
xs:schemaLocation="urn:net.woggioni.gbcs-memcached jpms://net.woggioni.gbcs.memcached/net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server.memcached jpms://net.woggioni.gbcs.server.memcached/net/woggioni/gbcs/server/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443" /> <bind host="127.0.0.1" port="11443" />
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="101325" digest="SHA-256"> <cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="101325" digest="SHA-256">
<server host="127.0.0.1" port="11211"/> <server host="127.0.0.1" port="11211"/>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" <gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs" xmlns:gbcs="urn:net.woggioni.gbcs.server"
xs:schemaLocation="urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs.server jpms://net.woggioni.gbcs.server/net/woggioni/gbcs/server/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <bind host="127.0.0.1" port="11443"/>
<cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/> <cache xs:type="gbcs:fileSystemCacheType" path="/tmp/gbcs" max-age="P7D"/>
<authorization> <authorization>

View File

@@ -2,9 +2,11 @@ org.gradle.configuration-cache=false
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
gbcs.version = 0.0.1 gbcs.version = 0.0.8
lys.version = 2025.01.10 lys.version = 2025.01.17
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
docker.registry.url=gitea.woggioni.net docker.registry.url=gitea.woggioni.net
jpms-check.configurationName = runtimeClasspath

View File

@@ -27,9 +27,9 @@ dependencyResolutionManagement {
rootProject.name = 'gbcs' rootProject.name = 'gbcs'
include 'gbcs-api' include 'gbcs-api'
include 'gbcs-base' include 'gbcs-common'
include 'gbcs-memcached' include 'gbcs-server-memcached'
include 'gbcs-cli' include 'gbcs-cli'
include 'docker' include 'docker'
include 'benchmark' include 'gbcs-client'
include 'gbcs-server'

View File

@@ -1 +0,0 @@
net.woggioni.gbcs.cache.FileSystemCacheProvider