diff --git a/benchmark/build.gradle b/benchmark/build.gradle deleted file mode 100644 index bcde4ed..0000000 --- a/benchmark/build.gradle +++ /dev/null @@ -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' -} - diff --git a/benchmark/src/jmh/resources/benchmark.properties b/benchmark/src/jmh/resources/benchmark.properties deleted file mode 100644 index 51f0799..0000000 --- a/benchmark/src/jmh/resources/benchmark.properties +++ /dev/null @@ -1 +0,0 @@ -gbcs.server.url= http://localhost:8080 \ No newline at end of file diff --git a/gbcs-cli/build.gradle b/gbcs-cli/build.gradle index 7067b7e..bec4d39 100644 --- a/gbcs-cli/build.gradle +++ b/gbcs-cli/build.gradle @@ -7,12 +7,11 @@ plugins { id 'maven-publish' } + import net.woggioni.gradle.envelope.EnvelopeJarTask +import net.woggioni.gradle.graalvm.NativeImageConfigurationTask import net.woggioni.gradle.graalvm.NativeImagePlugin import net.woggioni.gradle.graalvm.NativeImageTask -import net.woggioni.gradle.graalvm.NativeImageConfigurationTask -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.gradle.dsl.JvmTarget Property mainClassName = objects.property(String.class) mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli') @@ -43,6 +42,7 @@ dependencies { implementation catalog.netty.codec.http implementation catalog.picocli + implementation project(":gbcs-client") implementation rootProject // runtimeOnly catalog.slf4j.jdk14 diff --git a/gbcs-cli/src/main/java/module-info.java b/gbcs-cli/src/main/java/module-info.java index f105f90..51539bd 100644 --- a/gbcs-cli/src/main/java/module-info.java +++ b/gbcs-cli/src/main/java/module-info.java @@ -3,6 +3,7 @@ module net.woggioni.gbcs.cli { requires net.woggioni.gbcs; requires info.picocli; requires net.woggioni.gbcs.base; + requires net.woggioni.gbcs.client; requires kotlin.stdlib; requires net.woggioni.jwo; diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt index 7b41296..a2329a1 100644 --- a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt @@ -8,6 +8,8 @@ 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.GbcsCommand +import net.woggioni.gbcs.cli.impl.commands.BenchmarkCommand +import net.woggioni.gbcs.cli.impl.commands.ClientCommand import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand import net.woggioni.jwo.Application import net.woggioni.jwo.JWO @@ -22,7 +24,7 @@ import java.nio.file.Path @CommandLine.Command( name = "gbcs", versionProvider = GradleBuildCacheServerCli.VersionProvider::class ) -class GradleBuildCacheServerCli(application : Application, private val log : Logger) : GbcsCommand() { +class GradleBuildCacheServerCli(application: Application, private val log: Logger) : GbcsCommand() { class VersionProvider : AbstractVersionProvider() companion object { @@ -42,6 +44,15 @@ class GradleBuildCacheServerCli(application : Application, private val log : Log CommandLine.ExitCode.SOFTWARE } commandLine.addSubcommand(PasswordHashCommand()) + val clientApp = Application.builder("gbcs-client") + .configurationDirectoryEnvVar("GBCS_CLIENT_CONFIGURATION_DIR") + .configurationDirectoryPropertyKey("net.woggioni.gbcs.client.conf.dir") + .build() + + commandLine.addSubcommand( + CommandLine(ClientCommand(clientApp)).apply { + addSubcommand(BenchmarkCommand()) + }) System.exit(commandLine.execute(*args)) } } @@ -60,13 +71,13 @@ class GradleBuildCacheServerCli(application : Application, private val log : Log @CommandLine.Spec private lateinit var spec: CommandSpec - private fun findConfigurationFile(app : Application): Path { + private fun findConfigurationFile(app: Application): Path { val confDir = app.computeConfigurationDirectory() val configurationFile = confDir.resolve("gbcs.xml") return configurationFile } - private fun createDefaultConfigurationFile(configurationFile : Path) { + private fun createDefaultConfigurationFile(configurationFile: Path) { log.info { "Creating default configuration file at '$configurationFile'" } diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt new file mode 100644 index 0000000..b654894 --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/BenchmarkCommand.kt @@ -0,0 +1,119 @@ +package net.woggioni.gbcs.cli.impl.commands + +import net.woggioni.gbcs.base.contextLogger +import net.woggioni.gbcs.base.error +import net.woggioni.gbcs.base.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 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>>(numberOfEntries) + val start = Instant.now() + entryGenerator.take(numberOfEntries).forEach { entry -> + val future = client.put(entry.first, entry.second).thenApply { entry } + future.whenComplete { _, _ -> + completionQueue.put(future) + } + } + + val inserted = sequence> { + 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" + } + inserted + } + log.info { + "Inserted ${entries.size} entries" + } + if (entries.isNotEmpty()) { + val completionQueue = LinkedBlockingQueue>(entries.size) + val start = Instant.now() + entries.forEach { entry -> + val future = client.get(entry.first).thenApply { + 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" + } + } else { + log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache") + } + } +} \ No newline at end of file diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/ClientCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/ClientCommand.kt new file mode 100644 index 0000000..06312d7 --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/ClientCommand.kt @@ -0,0 +1,50 @@ +package net.woggioni.gbcs.cli.impl.commands + +import net.woggioni.gbcs.client.GbcsClient + +import net.woggioni.gbcs.cli.impl.GbcsCommand +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() { + + companion object { + private fun findConfigurationFile(app: Application): Path { + val confDir = app.computeConfigurationDirectory() + val configurationFile = confDir.resolve("gbcs-client.xml") + return configurationFile + } + } + + @CommandLine.Option( + names = ["-c", "--configuration"], + description = ["Path to the client configuration file"], + paramLabel = "CONFIGURATION_FILE" + ) + private var configurationFile : Path = findConfigurationFile(app) + + @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) + } + } +} \ No newline at end of file diff --git a/gbcs-client/build.gradle b/gbcs-client/build.gradle new file mode 100644 index 0000000..434e069 --- /dev/null +++ b/gbcs-client/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'java-library' + alias catalog.plugins.kotlin.jvm +} + +dependencies { + implementation project(':gbcs-base') + implementation catalog.picocli + implementation catalog.netty.buffer + implementation catalog.netty.codec.http +} + + diff --git a/benchmark/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java b/gbcs-client/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java similarity index 100% rename from benchmark/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java rename to gbcs-client/src/jmh/java/net/woggioni/gbcs/benchmark/Main.java diff --git a/gbcs-client/src/jmh/resources/benchmark.properties b/gbcs-client/src/jmh/resources/benchmark.properties new file mode 100644 index 0000000..aa7d29e --- /dev/null +++ b/gbcs-client/src/jmh/resources/benchmark.properties @@ -0,0 +1,6 @@ +gbcs.server.url= https://gbcs.woggioni.net:443 +gbcs.client.ssl.keystore.file=conf/woggioni@c962475fa38.p12 +gbcs.client.ssl.keystore.password=password +gbcs.client.ssl.key.password=password +gbcs.client.ssl.truststore.file=conf/truststore.pfx +gbcs.client.ssl.truststore.password=password \ No newline at end of file diff --git a/gbcs-client/src/main/java/module-info.java b/gbcs-client/src/main/java/module-info.java new file mode 100644 index 0000000..6d6089c --- /dev/null +++ b/gbcs-client/src/main/java/module-info.java @@ -0,0 +1,15 @@ +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.base; + requires io.netty.codec; + + exports net.woggioni.gbcs.client; + + opens net.woggioni.gbcs.client.schema; +} \ No newline at end of file diff --git a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt new file mode 100644 index 0000000..4602dc5 --- /dev/null +++ b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Client.kt @@ -0,0 +1,209 @@ +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.ChannelInitializer +import io.netty.channel.ChannelPipeline +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +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 net.woggioni.gbcs.base.Xml +import net.woggioni.gbcs.client.impl.Parser +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 io.netty.util.concurrent.Future as NettyFuture + + +class GbcsClient(private val profile: Configuration.Profile) : AutoCloseable { + private val group: NioEventLoopGroup + private var sslContext: SslContext + + + data class Configuration( + val profiles : Map + ) { + sealed class Authentication { + data class TlsClientAuthenticationCredentials(val key: PrivateKey, val certificateChain: Array) : Authentication() + data class BasicAuthenticationCredentials(val username: String, val password: String) : Authentication() + } + + data class Profile( + val serverURI: URI, + val authentication : Authentication? + ) + + companion object { + fun parse(path : Path) : Configuration { + return Files.newInputStream(path).use { + Xml.parseXml(path.toUri().toURL(), it) + }.let(Parser::parse) + } + } + } + + init { + group = NioEventLoopGroup() + + this.sslContext = SslContextBuilder.forClient().also { builder -> + + (profile.authentication as? Configuration.Authentication.TlsClientAuthenticationCredentials)?.let { tlsClientAuthenticationCredentials -> + builder.keyManager( + tlsClientAuthenticationCredentials.key, + *tlsClientAuthenticationCredentials.certificateChain + ) + } + }.build() + } + + fun get(key: String): CompletableFuture { + 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 { + 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 { + val responseFuture = CompletableFuture() + + try { + val scheme = if (uri.scheme == null) "http" else uri.scheme + val host = uri.host + var port = uri.port + if (port == -1) { + port = if ("https".equals(scheme, ignoreCase = true)) 443 else 80 + } + + val bootstrap = Bootstrap() + bootstrap.group(group) + .channel(NioSocketChannel::class.java) + .handler(object : ChannelInitializer() { + override fun initChannel(ch: SocketChannel) { + 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()) + + + // Custom handler for processing responses + pipeline.addLast("handler", object : SimpleChannelInboundHandler() { + override fun channelRead0( + ctx: ChannelHandlerContext, + response: FullHttpResponse + ) { + responseFuture.complete(response) + ctx.close() + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + val ex = when (cause) { + is DecoderException -> cause.cause + else -> cause + } + responseFuture.completeExceptionally(ex) + ctx.close() + } + }) + } + }) + + // Connect to host + val channel: Channel = bootstrap.connect(host, port).sync().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, 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) + } catch (e: Exception) { + responseFuture.completeExceptionally(e) + } + + return responseFuture + } + + fun shutDown(): NettyFuture<*> { + return group.shutdownGracefully() + } + + override fun close() { + shutDown().sync() + } +} \ No newline at end of file diff --git a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Exception.kt b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Exception.kt new file mode 100644 index 0000000..cd91dbc --- /dev/null +++ b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Exception.kt @@ -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()}" +} \ No newline at end of file diff --git a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Main.kt b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Main.kt new file mode 100644 index 0000000..575db87 --- /dev/null +++ b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/Main.kt @@ -0,0 +1,38 @@ +package net.woggioni.gbcs.client +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 +import kotlin.random.Random + + +//object Main { +// @JvmStatic +// fun main(vararg args : String) { +// val pwd = "PO%!*bW9p'Zp#=uu\$fl{Ij`Ad.8}x#ho".toCharArray() +// val keystore = KeyStore.getInstance("PKCS12").apply{ +// Files.newInputStream(Path.of("/home/woggioni/ssl/woggioni@c962475fa38.pfx")).use { +// load(it, pwd) +// } +// } +// val key = keystore.getKey("woggioni@c962475fa38", pwd) as PrivateKey +// val certChain = keystore.getCertificateChain("woggioni@c962475fa38").asSequence() +// .map { it as X509Certificate } +// .toList() +// .toTypedArray() +// GbcsClient.Configuration( +// serverURI = URI("https://gbcs.woggioni.net/"), +// GbcsClient.TlsClientAuthenticationCredentials( +// key, certChain +// ) +// ).let(::GbcsClient).use { client -> +// val random = Random(101325) +// val entry = "something" to ByteArray(0x1000).also(random::nextBytes) +// client.put(entry.first, entry.second) +// val retrieved = client.get(entry.first).get() +// println(retrieved.contentEquals(entry.second)) +// } +// } +//} \ No newline at end of file diff --git a/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt new file mode 100644 index 0000000..5f34882 --- /dev/null +++ b/gbcs-client/src/main/kotlin/net/woggioni/gbcs/client/impl/Parser.kt @@ -0,0 +1,64 @@ +package net.woggioni.gbcs.client.impl + +import net.woggioni.gbcs.base.Xml.Companion.asIterable +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() + + for (child in root.asIterable()) { + val tagName = child.localName + when (tagName) { + "profile" -> { + val name = child.getAttribute("name") + val uri = child.getAttribute("base-url").let(::URI) + var authentication: GbcsClient.Configuration.Authentication? = null + for (gchild in child.asIterable()) { + when (gchild.localName) { + "tls-client-auth" -> { + val keyStoreFile = gchild.getAttribute("key-store-file") + val keyStorePassword = + gchild.getAttribute("key-store-password").takeIf(String::isNotEmpty) + val keyAlias = gchild.getAttribute("key-alias") + val keyPassword = gchild.getAttribute("key-password").takeIf(String::isNotEmpty) + + 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.getAttribute("user") + val password = gchild.getAttribute("password") + authentication = + GbcsClient.Configuration.Authentication.BasicAuthenticationCredentials(username, password) + } + } + } + profiles[name] = GbcsClient.Configuration.Profile(uri, authentication) + } + } + } + return GbcsClient.Configuration(profiles) + } +} \ No newline at end of file diff --git a/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd b/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd new file mode 100644 index 0000000..5927d33 --- /dev/null +++ b/gbcs-client/src/main/resources/net/woggioni/gbcs/client/schema/gbcs-client.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gbcs-client/src/test/resources/net/woggioni/gbcs/client/test/gbcs-client.xml b/gbcs-client/src/test/resources/net/woggioni/gbcs/client/test/gbcs-client.xml new file mode 100644 index 0000000..037df85 --- /dev/null +++ b/gbcs-client/src/test/resources/net/woggioni/gbcs/client/test/gbcs-client.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index ce15f6e..d3c0b55 100644 --- a/settings.gradle +++ b/settings.gradle @@ -31,5 +31,5 @@ include 'gbcs-base' include 'gbcs-memcached' include 'gbcs-cli' include 'docker' -include 'benchmark' +include 'gbcs-client'