Compare commits

...

2 Commits

Author SHA1 Message Date
d701157b06 added jpms url protocol
Some checks failed
CI / build (push) Successful in 31s
CI / Build Docker images (push) Failing after 15s
2025-01-10 17:09:40 +08:00
01d5b1462c added dedicated cli module 2025-01-09 16:58:02 +08:00
37 changed files with 561 additions and 367 deletions

View File

@@ -5,6 +5,8 @@ on:
- '*' - '*'
jobs: jobs:
build: build:
env:
RUNNER_TOOL_CACHE: /toolcache
runs-on: hostinger runs-on: hostinger
steps: steps:
- name: Checkout sources - name: Checkout sources
@@ -19,7 +21,11 @@ jobs:
- name: Execute Gradle build - name: Execute Gradle build
env: env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
run: ./gradlew build publish run: ./gradlew build
- name: Publish artifacts
env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
run: ./gradlew publish
build-docker: build-docker:
name: "Build Docker images" name: "Build Docker images"
runs-on: hostinger runs-on: hostinger

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build
gbcs-cli/native-image/*.json

View File

@@ -9,11 +9,11 @@ WORKDIR /home/ubuntu
RUN mkdir gbcs RUN mkdir gbcs
WORKDIR /home/ubuntu/gbcs WORKDIR /home/ubuntu/gbcs
COPY --chown=ubuntu:users native-image native-image
COPY --chown=ubuntu:users .git .git COPY --chown=ubuntu:users .git .git
COPY --chown=ubuntu:users gbcs-base gbcs-base COPY --chown=ubuntu:users gbcs-base gbcs-base
COPY --chown=ubuntu:users gbcs-api gbcs-api COPY --chown=ubuntu:users gbcs-api gbcs-api
COPY --chown=ubuntu:users gbcs-memcached gbcs-memcached COPY --chown=ubuntu:users gbcs-memcached gbcs-memcached
COPY --chown=ubuntu:users gbcs-cli gbcs-cli
COPY --chown=ubuntu:users src src COPY --chown=ubuntu:users src src
COPY --chown=ubuntu:users settings.gradle settings.gradle COPY --chown=ubuntu:users settings.gradle settings.gradle
COPY --chown=ubuntu:users build.gradle build.gradle COPY --chown=ubuntu:users build.gradle build.gradle
@@ -31,11 +31,11 @@ USER luser
WORKDIR /home/luser WORKDIR /home/luser
FROM base-release AS release FROM base-release AS release
RUN --mount=type=bind,from=build,source=/home/ubuntu/gbcs/build,target=/home/luser/build cp build/libs/gbcs-envelope*.jar gbcs.jar RUN --mount=type=bind,from=build,source=/home/ubuntu/gbcs/gbcs-cli/build,target=/home/luser/build cp build/libs/gbcs-cli-envelope-*.jar gbcs.jar
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar"] ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar"]
FROM base-release AS release-memcached FROM base-release AS release-memcached
RUN --mount=type=bind,from=build,source=/home/ubuntu/gbcs/build,target=/home/luser/build cp build/libs/gbcs-envelope*.jar gbcs.jar RUN --mount=type=bind,from=build,source=/home/ubuntu/gbcs/gbcs-cli/build,target=/home/luser/build cp build/libs/gbcs-cli-envelope-*.jar gbcs.jar
RUN mkdir plugins RUN mkdir plugins
WORKDIR /home/luser/plugins WORKDIR /home/luser/plugins
RUN --mount=type=bind,from=build,source=/home/ubuntu/gbcs/gbcs-memcached/build/distributions,target=/build/distributions tar -xf /build/distributions/gbcs-memcached*.tar RUN --mount=type=bind,from=build,source=/home/ubuntu/gbcs/gbcs-memcached/build/distributions,target=/build/distributions tar -xf /build/distributions/gbcs-memcached*.tar

View File

@@ -1,32 +1,25 @@
plugins { plugins {
id 'java-library' id 'java-library'
id 'jvm-test-suite'
alias catalog.plugins.kotlin.jvm alias catalog.plugins.kotlin.jvm
alias catalog.plugins.envelope
alias catalog.plugins.sambal alias catalog.plugins.sambal
alias catalog.plugins.lombok alias catalog.plugins.lombok
alias catalog.plugins.graalvm.native.image
id 'maven-publish' id 'maven-publish'
} }
import net.woggioni.gradle.envelope.EnvelopeJarTask
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.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
allprojects { allprojects { subproject ->
group = 'net.woggioni' group = 'net.woggioni'
if(project.currentTag.isPresent()) { if(project.currentTag.isPresent()) {
version = project.currentTag version = project.currentTag.map { it[0] }.get()
} else { } else {
version = project.gitRevision.map { gitRevision -> version = project.gitRevision.map { gitRevision ->
"${getProperty('gbcs.version')}.${gitRevision[0..10]}" "${getProperty('gbcs.version')}.${gitRevision[0..10]}"
}.get()
} }
}
repositories { repositories {
maven { maven {
@@ -41,6 +34,11 @@ allprojects {
} }
pluginManager.withPlugin('java-library') { pluginManager.withPlugin('java-library') {
ext {
jpmsModuleName = subproject.group + '.' + subproject.name.replace('-', '.')
}
java { java {
withSourcesJar() withSourcesJar()
modularity.inferModulePath = true modularity.inferModulePath = true
@@ -58,6 +56,16 @@ allprojects {
modularity.inferModulePath = true modularity.inferModulePath = true
options.release = 21 options.release = 21
} }
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.compilerArgumentProviders << new CommandLineArgumentProvider() {
@Override
Iterable<String> asArguments() {
return ['--patch-module', subproject.jpmsModuleName + '=' + subproject.sourceSets.main.output.asPath]
}
}
options.javaModuleVersion = version
}
} }
pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) { pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) {
@@ -94,32 +102,16 @@ allprojects {
} }
} }
Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.gbcs.GradleBuildCacheServer')
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs=' + project.sourceSets.main.output.asPath
options.javaModuleVersion = version
options.javaModuleMainClass = mainClassName
}
envelopeJar {
mainModule = 'net.woggioni.gbcs'
mainClass = mainClassName
extraClasspath = ["plugins"]
}
dependencies { dependencies {
implementation catalog.jwo implementation catalog.jwo
implementation catalog.slf4j.api implementation catalog.slf4j.api
implementation catalog.netty.codec.http implementation catalog.netty.codec.http
implementation project('gbcs-base') api project('gbcs-base')
implementation project('gbcs-api') api project('gbcs-api')
// runtimeOnly catalog.slf4j.jdk14 // runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic testRuntimeOnly catalog.logback.classic
testImplementation catalog.bcprov.jdk18on testImplementation catalog.bcprov.jdk18on
testImplementation catalog.bcpkix.jdk18on testImplementation catalog.bcpkix.jdk18on
@@ -130,32 +122,10 @@ dependencies {
testRuntimeOnly project("gbcs-memcached") testRuntimeOnly project("gbcs-memcached")
} }
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
// systemProperties['log.config.source'] = 'logging.properties'
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/logback.xml'
}
def envelopeJarArtifact = artifacts.add('archives', envelopeJarTaskProvider.get().archiveFile.get().asFile) {
type = 'jar'
builtBy envelopeJarTaskProvider
}
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
mainClass = 'net.woggioni.gbcs.GraalNativeImageConfiguration'
}
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer'
useMusl = true
buildStaticImage = true
}
publishing { publishing {
publications { publications {
maven(MavenPublication) { maven(MavenPublication) {
artifact envelopeJarArtifact from(components["java"])
} }
} }
} }

View File

@@ -2,7 +2,7 @@
<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"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached" xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached"
xs:schemaLocation="urn:net.woggioni.gbcs-memcached classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd urn:net.woggioni.gbcs classpath:net/woggioni/gbcs/schema/gbcs.xsd"> 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">
<bind host="0.0.0.0" port="13080" /> <bind host="0.0.0.0" port="13080" />
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="16777216" compression-mode="zip"> <cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="16777216" compression-mode="zip">
<server host="memcached" port="11211"/> <server host="memcached" port="11211"/>

View File

@@ -1,11 +1,16 @@
plugins { plugins {
id 'java-library' id 'java-library'
id 'maven-publish'
alias catalog.plugins.lombok alias catalog.plugins.lombok
} }
dependencies { dependencies {
} }
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { publishing {
options.javaModuleVersion = version publications {
maven(MavenPublication) {
from(components["java"])
}
}
} }

View File

@@ -1,6 +1,7 @@
package net.woggioni.gbcs.api; package net.woggioni.gbcs.api;
import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import java.nio.file.Path; import java.nio.file.Path;
@@ -23,25 +24,18 @@ public class Configuration {
@Value @Value
public static class Group { public static class Group {
@EqualsAndHashCode.Include
String name; String name;
Set<Role> roles; Set<Role> roles;
@Override
public int hashCode() {
return name.hashCode();
}
} }
@Value @Value
public static class User { public static class User {
@EqualsAndHashCode.Include
String name; String name;
String password; String password;
Set<Group> groups; Set<Group> groups;
@Override
public int hashCode() {
return name.hashCode();
}
public Set<Role> getRoles() { public Set<Role> getRoles() {
return groups.stream() return groups.stream()

View File

@@ -1,8 +1,7 @@
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'
alias catalog.plugins.kotlin.jvm alias catalog.plugins.kotlin.jvm
} }
@@ -11,11 +10,10 @@ dependencies {
compileOnly catalog.slf4j.api compileOnly catalog.slf4j.api
} }
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { publishing {
options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs.base=' + project.sourceSets.main.output.asPath publications {
options.javaModuleVersion = version maven(MavenPublication) {
} from(components["java"])
}
tasks.named("compileKotlin", KotlinCompile.class) { }
compilerOptions.jvmTarget = JvmTarget.JVM_21
} }

View File

@@ -0,0 +1,108 @@
package net.woggioni.gbcs.base
import java.io.IOException
import java.io.InputStream
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.net.URLStreamHandlerFactory
import java.util.Optional
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.Collectors
class GbcsUrlStreamHandlerFactory : URLStreamHandlerFactory {
private class ClasspathHandler(private val classLoader: ClassLoader = GbcsUrlStreamHandlerFactory::class.java.classLoader) :
URLStreamHandler() {
override fun openConnection(u: URL): URLConnection? {
return javaClass.module
?.takeIf { m: Module -> m.layer != null }
?.let {
val path = u.path
val i = path.lastIndexOf('/')
val packageName = path.substring(0, i).replace('/', '.')
val modules = packageMap[packageName]!!
ClasspathResourceURLConnection(
u,
modules
)
}
?: classLoader.getResource(u.path)?.let(URL::openConnection)
}
}
private class JpmsHandler : URLStreamHandler() {
override fun openConnection(u: URL): URLConnection {
val thisModule = javaClass.module
val sourceModule = Optional.ofNullable(thisModule)
.map { obj: Module -> obj.layer }
.flatMap { layer: ModuleLayer ->
val moduleName = u.host
layer.findModule(moduleName)
}.orElse(thisModule)
return JpmsResourceURLConnection(u, sourceModule)
}
}
private class JpmsResourceURLConnection(url: URL, private val module: Module) : URLConnection(url) {
override fun connect() {
}
@Throws(IOException::class)
override fun getInputStream(): InputStream {
return module.getResourceAsStream(getURL().path)
}
}
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
return when (protocol) {
"classpath" -> ClasspathHandler()
"jpms" -> JpmsHandler()
else -> null
}
}
private class ClasspathResourceURLConnection(url: URL?, private val modules: List<Module>) :
URLConnection(url) {
override fun connect() {}
override fun getInputStream(): InputStream? {
for (module in modules) {
val result = module.getResourceAsStream(getURL().path)
if (result != null) return result
}
return null
}
}
companion object {
private val installed = AtomicBoolean(false)
fun install() {
if (!installed.getAndSet(true)) {
URL.setURLStreamHandlerFactory(GbcsUrlStreamHandlerFactory())
}
}
private val packageMap: Map<String, List<Module>> by lazy {
GbcsUrlStreamHandlerFactory::class.java.module.layer
.modules()
.stream()
.flatMap { m: Module ->
m.packages.stream()
.map { p: String -> p to m }
}
.collect(
Collectors.groupingBy(
Pair<String, Module>::first,
Collectors.mapping(
Pair<String, Module>::second,
Collectors.toUnmodifiableList<Module>()
)
)
)
}
}
}

View File

@@ -0,0 +1,46 @@
package net.woggioni.gbcs.base
import java.security.SecureRandom
import java.security.spec.KeySpec
import java.util.Base64
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
object PasswordSecurity {
private const val KEY_LENGTH = 256
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
val result = ByteArray(arr1.size + arr2.size)
var j = 0
for(element in arr1) {
result[j] = element
j += 1
}
for(element in arr2) {
result[j] = element
j += 1
}
return result
}
fun hashPassword(password : String, salt : String? = null) : String {
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
val result = ByteArray(16)
nextBytes(result)
result
}
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val hash = factory.generateSecret(spec).encoded
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
}
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(passwordHash)
val hash = ByteArray(KEY_LENGTH / 8)
val salt = ByteArray(decoded.size - KEY_LENGTH / 8)
System.arraycopy(decoded, 0, hash, 0, hash.size)
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
return hash to salt
}
}

66
gbcs-cli/build.gradle Normal file
View File

@@ -0,0 +1,66 @@
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
alias catalog.plugins.envelope
alias catalog.plugins.sambal
alias catalog.plugins.graalvm.native.image
id 'maven-publish'
}
import net.woggioni.gradle.envelope.EnvelopeJarTask
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<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleMainClass = mainClassName
}
envelopeJar {
mainModule = 'net.woggioni.gbcs.cli'
mainClass = mainClassName
extraClasspath = ["plugins"]
}
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.netty.codec.http
implementation catalog.picocli
implementation rootProject
// runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic
}
Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', EnvelopeJarTask.class) {
// systemProperties['java.util.logging.config.class'] = 'net.woggioni.gbcs.LoggingConfig'
// systemProperties['log.config.source'] = 'logging.properties'
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/cli/logback.xml'
}
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
mainClass = 'net.woggioni.gbcs.GraalNativeImageConfiguration'
}
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer'
useMusl = true
buildStaticImage = true
}
publishing {
publications {
maven(MavenPublication) {
artifact envelopeJar
}
}
}

View File

@@ -0,0 +1,15 @@
module net.woggioni.gbcs.cli {
requires org.slf4j;
requires net.woggioni.gbcs;
requires info.picocli;
requires net.woggioni.gbcs.base;
requires kotlin.stdlib;
requires net.woggioni.jwo;
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 to info.picocli;
opens net.woggioni.gbcs.cli to info.picocli, net.woggioni.gbcs.base;
exports net.woggioni.gbcs.cli;
}

View File

@@ -0,0 +1,98 @@
package net.woggioni.gbcs.cli
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
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.GbcsCommand
import net.woggioni.gbcs.cli.impl.commands.PasswordHashCommand
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import org.slf4j.Logger
import picocli.CommandLine
import picocli.CommandLine.Model.CommandSpec
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.nio.file.Path
@CommandLine.Command(
name = "gbcs", versionProvider = GradleBuildCacheServerCli.VersionProvider::class
)
class GradleBuildCacheServerCli(application : Application, private val log : Logger) : GbcsCommand() {
class VersionProvider : AbstractVersionProvider()
companion object {
@JvmStatic
fun main(vararg args: String) {
Thread.currentThread().contextClassLoader = GradleBuildCacheServerCli::class.java.classLoader
GbcsUrlStreamHandlerFactory.install()
val log = contextLogger()
val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
.configurationDirectoryPropertyKey("net.woggioni.gbcs.conf.dir")
.build()
val gbcsCli = GradleBuildCacheServerCli(app, log)
val commandLine = CommandLine(gbcsCli)
commandLine.setExecutionExceptionHandler { ex, cl, parseResult ->
log.error(ex.message, ex)
CommandLine.ExitCode.SOFTWARE
}
commandLine.addSubcommand(PasswordHashCommand())
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)
var versionHelp = false
private set
@CommandLine.Spec
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() {
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())}"
}
}
GradleBuildCacheServer(configuration).run().use {
}
}
}

View File

@@ -0,0 +1,32 @@
package net.woggioni.gbcs.cli.impl
import picocli.CommandLine
import java.net.URL
import java.util.Enumeration
import java.util.jar.Attributes
import java.util.jar.JarFile
import java.util.jar.Manifest
abstract class AbstractVersionProvider : CommandLine.IVersionProvider {
private val version: String
private val vcsHash: String
init {
val mf = Manifest()
javaClass.module.getResourceAsStream(JarFile.MANIFEST_NAME).use { `is` ->
mf.read(`is`)
}
val mainAttributes = mf.mainAttributes
version = mainAttributes.getValue(Attributes.Name.SPECIFICATION_VERSION) ?: throw RuntimeException("Version information not found in manifest")
vcsHash = mainAttributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION) ?: throw RuntimeException("Version information not found in manifest")
}
override fun getVersion(): Array<String?> {
return if (version.endsWith("-SNAPSHOT")) {
arrayOf(version, vcsHash)
} else {
arrayOf(version)
}
}
}

View File

@@ -0,0 +1,11 @@
package net.woggioni.gbcs.cli.impl
import picocli.CommandLine
abstract class GbcsCommand : Runnable {
@CommandLine.Option(names = ["-h", "--help"], usageHelp = true)
var usageHelp = false
private set
}

View File

@@ -0,0 +1,38 @@
package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.cli.impl.converters.OutputStreamConverter
import net.woggioni.jwo.UncloseableOutputStream
import picocli.CommandLine
import java.io.BufferedWriter
import java.io.OutputStream
import java.io.OutputStreamWriter
@CommandLine.Command(
name = "password",
description = ["Generate a password hash to add to GBCS configuration file"],
showDefaultValues = true
)
class PasswordHashCommand : GbcsCommand() {
@CommandLine.Option(
names = ["-o", "--output-file"],
description = ["Write the output to a file instead of stdout"],
converter = [OutputStreamConverter::class],
defaultValue = "stdout",
paramLabel = "OUTPUT_FILE"
)
private var outputStream: OutputStream = UncloseableOutputStream(System.out)
override fun run() {
val password1 = String(System.console().readPassword("Type your password:"))
val password2 = String(System.console().readPassword("Type your password again for confirmation:"))
if(password1 != password2) throw IllegalArgumentException("Passwords do not match")
BufferedWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)).use {
it.write(hashPassword(password1))
it.newLine()
}
}
}

View File

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

View File

@@ -6,7 +6,7 @@
<import class="ch.qos.logback.core.ConsoleAppender"/> <import class="ch.qos.logback.core.ConsoleAppender"/>
<appender name="console" class="ConsoleAppender"> <appender name="console" class="ConsoleAppender">
<target>System.out</target> <target>System.err</target>
<encoder class="PatternLayoutEncoder"> <encoder class="PatternLayoutEncoder">
<pattern>%d [%highlight(%-5level)] \(%thread\) %logger{36} -%kvp- %msg %n</pattern> <pattern>%d [%highlight(%-5level)] \(%thread\) %logger{36} -%kvp- %msg %n</pattern>
</encoder> </encoder>

View File

@@ -30,15 +30,6 @@ dependencies {
implementation catalog.xmemcached implementation catalog.xmemcached
} }
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs.memcached=' + project.sourceSets.main.output.asPath
options.javaModuleVersion = version
}
tasks.named("compileKotlin", KotlinCompile.class) {
compilerOptions.jvmTarget = JvmTarget.JVM_21
}
Provider<Tar> bundleTask = tasks.register("bundle", Tar) { Provider<Tar> bundleTask = tasks.register("bundle", Tar) {
from(tasks.named(JavaPlugin.JAR_TASK_NAME)) from(tasks.named(JavaPlugin.JAR_TASK_NAME))
from(configurations.bundle) from(configurations.bundle)
@@ -49,16 +40,10 @@ tasks.named(BasePlugin.ASSEMBLE_TASK_NAME) {
dependsOn(bundleTask) dependsOn(bundleTask)
} }
def bundleArtifact = artifacts.add('archives', bundleTask.get().archiveFile.get().asFile) {
type = 'tar'
builtBy bundleTask
}
publishing { publishing {
publications { publications {
maven(MavenPublication) { maven(MavenPublication) {
artifact bundleArtifact artifact bundleTask
} }
} }
} }

View File

@@ -4,6 +4,6 @@ org.gradle.caching=true
gbcs.version = 0.0.1 gbcs.version = 0.0.1
lys.version = 2025.01.08 lys.version = 2025.01.09
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven

View File

@@ -29,5 +29,6 @@ rootProject.name = 'gbcs'
include 'gbcs-api' include 'gbcs-api'
include 'gbcs-base' include 'gbcs-base'
include 'gbcs-memcached' include 'gbcs-memcached'
include 'gbcs-cli'

View File

@@ -1,8 +1,7 @@
import net.woggioni.gbcs.api.CacheProvider; import net.woggioni.gbcs.api.CacheProvider;
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider;
import net.woggioni.gbcs.cache.FileSystemCacheProvider; import net.woggioni.gbcs.cache.FileSystemCacheProvider;
open module net.woggioni.gbcs { module net.woggioni.gbcs {
requires java.sql; requires java.sql;
requires java.xml; requires java.xml;
requires java.logging; requires java.logging;
@@ -19,9 +18,11 @@ open module net.woggioni.gbcs {
requires net.woggioni.gbcs.base; requires net.woggioni.gbcs.base;
requires net.woggioni.gbcs.api; requires net.woggioni.gbcs.api;
provides java.net.URLStreamHandlerFactory with ClasspathUrlStreamHandlerFactoryProvider; exports net.woggioni.gbcs;
uses java.net.URLStreamHandlerFactory;
uses CacheProvider;
opens net.woggioni.gbcs;
opens net.woggioni.gbcs.schema;
uses CacheProvider;
provides CacheProvider with FileSystemCacheProvider; provides CacheProvider with FileSystemCacheProvider;
} }

View File

@@ -1,104 +0,0 @@
package net.woggioni.gbcs.url;
import net.woggioni.jwo.Fun;
import net.woggioni.jwo.LazyValue;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
public class ClasspathUrlStreamHandlerFactoryProvider implements URLStreamHandlerFactory {
private static final AtomicBoolean installed = new AtomicBoolean(false);
public static void install() {
if(!installed.getAndSet(true)) {
URL.setURLStreamHandlerFactory(new ClasspathUrlStreamHandlerFactoryProvider());
}
}
private static final LazyValue<Map<String, List<Module>>> packageMap = LazyValue.of(() ->
ClasspathUrlStreamHandlerFactoryProvider.class.getModule().getLayer()
.modules()
.stream()
.flatMap(m -> m.getPackages().stream().map(p -> Map.entry(p, m)))
.collect(
Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(
Map.Entry::getValue,
Collectors.toUnmodifiableList()
)
)
),
LazyValue.ThreadSafetyMode.NONE
);
private static class Handler extends URLStreamHandler {
private final ClassLoader classLoader;
public Handler() {
this.classLoader = getClass().getClassLoader();
}
public Handler(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
protected URLConnection openConnection(URL u) throws IOException {
return Optional.ofNullable(getClass().getModule())
.filter(m -> m.getLayer() != null)
.map(m -> {
final var path = u.getPath();
final var i = path.lastIndexOf('/');
final var packageName = path.substring(0, i).replace('/', '.');
final var modules = packageMap.get().get(packageName);
return (URLConnection) new ModuleResourceURLConnection(u, modules);
})
.or(() -> Optional.of(classLoader).map(cl -> cl.getResource(u.getPath())).map((Fun<URL, URLConnection>) URL::openConnection))
.orElse(null);
}
}
@Override
public URLStreamHandler createURLStreamHandler(String protocol) {
URLStreamHandler result;
switch (protocol) {
case "classpath":
result = new Handler();
break;
default:
result = null;
}
return result;
}
private static final class ModuleResourceURLConnection extends URLConnection {
private final List<Module> modules;
ModuleResourceURLConnection(URL url, List<Module> modules) {
super(url);
this.modules = modules;
}
public void connect() {
}
public InputStream getInputStream() throws IOException {
for(final var module : modules) {
final var result = module.getResourceAsStream(getURL().getPath());
if(result != null) return result;
}
return null;
}
}
}

View File

@@ -46,21 +46,21 @@ 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.auth.Authorizer
import net.woggioni.gbcs.auth.ClientCertificateValidator
import net.woggioni.gbcs.auth.RoleAuthorizer
import net.woggioni.gbcs.base.GBCS.toUrl import net.woggioni.gbcs.base.GBCS.toUrl
import net.woggioni.gbcs.base.PasswordSecurity.decodePasswordHash
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.contextLogger import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.configuration.Parser import net.woggioni.gbcs.configuration.Parser
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2 import net.woggioni.jwo.Tuple2
import java.io.ByteArrayOutputStream import java.io.OutputStream
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URL
import java.net.URLStreamHandlerFactory
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@@ -344,20 +344,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
companion object { companion object {
private val log = contextLogger() private val log = contextLogger()
private fun splitPath(req: HttpRequest): Pair<String?, String> {
val uri = req.uri()
val i = uri.lastIndexOf('/')
if (i < 0) throw RuntimeException(String.format("Malformed request URI: '%s'", uri))
return uri.substring(0, i) to uri.substring(i + 1)
}
} }
override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) { override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) {
val keepAlive: Boolean = HttpUtil.isKeepAlive(msg) val keepAlive: Boolean = HttpUtil.isKeepAlive(msg)
val method = msg.method() val method = msg.method()
if (method === HttpMethod.GET) { if (method === HttpMethod.GET) {
// val (prefix, key) = splitPath(msg)
val path = Path.of(msg.uri()) val path = Path.of(msg.uri())
val prefix = path.parent val prefix = path.parent
val key = path.fileName.toString() val key = path.fileName.toString()
@@ -453,15 +445,16 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
} }
class ServerHandle( class ServerHandle(
private val httpChannel: ChannelFuture, httpChannelFuture: ChannelFuture,
private val bossGroup: EventLoopGroup, private val bossGroup: EventLoopGroup,
private val workerGroup: EventLoopGroup private val workerGroup: EventLoopGroup
) : AutoCloseable { ) : AutoCloseable {
private val httpChannel: Channel = httpChannelFuture.channel()
private val closeFuture: ChannelFuture = httpChannel.channel().closeFuture() private val closeFuture: ChannelFuture = httpChannel.closeFuture()
fun shutdown(): ChannelFuture { fun shutdown(): ChannelFuture {
return httpChannel.channel().close() return httpChannel.close()
} }
override fun close() { override fun close() {
@@ -480,12 +473,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
fun run(): ServerHandle { fun run(): ServerHandle {
// Create the multithreaded event loops for the server // Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup() val bossGroup = NioEventLoopGroup(0, Executors.defaultThreadFactory())
val serverSocketChannel = NioServerSocketChannel::class.java val serverSocketChannel = NioServerSocketChannel::class.java
val workerGroup = if (cfg.isUseVirtualThread) { val workerGroup = if (cfg.isUseVirtualThread) {
NioEventLoopGroup(0, Executors.newVirtualThreadPerTaskExecutor()) NioEventLoopGroup(0, Executors.newVirtualThreadPerTaskExecutor())
} else { } else {
NioEventLoopGroup(0, Executors.newWorkStealingPool()) bossGroup
} }
// A helper class that simplifies server configuration // A helper class that simplifies server configuration
val bootstrap = ServerBootstrap().apply { val bootstrap = ServerBootstrap().apply {
@@ -506,91 +499,17 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
companion object { companion object {
private val log by lazy {
contextLogger()
}
private const val PROTOCOL_HANDLER = "java.protocol.handler.pkgs"
private const val HANDLERS_PACKAGE = "net.woggioni.gbcs.url"
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 {
* Reset any cached handlers just in case a jar protocol has already been used. We
* reset the handler by trying to set a null [URLStreamHandlerFactory] which
* should have no effect other than clearing the handlers cache.
*/
private fun resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null)
} catch (ex: Error) {
// Ignore
}
}
fun registerUrlProtocolHandler() {
val handlers = System.getProperty(PROTOCOL_HANDLER, "")
System.setProperty(
PROTOCOL_HANDLER,
if (handlers == null || handlers.isEmpty()) HANDLERS_PACKAGE else "$handlers|$HANDLERS_PACKAGE"
)
resetCachedUrlHandlers()
}
fun loadConfiguration(args: Array<String>): Configuration {
// Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader
val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
.configurationDirectoryPropertyKey("net.woggioni.gbcs.conf.dir")
.build()
val confDir = app.computeConfigurationDirectory()
val configurationFile = confDir.resolve("gbcs.xml")
if (!Files.exists(configurationFile)) {
log.info {
"Creating default configuration file at '$configurationFile'"
}
Files.createDirectories(confDir)
val defaultConfigurationFileResource = DEFAULT_CONFIGURATION_URL
Files.newOutputStream(configurationFile).use { outputStream ->
defaultConfigurationFileResource.openStream().use { inputStream ->
JWO.copy(inputStream, outputStream)
}
}
}
// val schemaUrl = javaClass.getResource("/net/woggioni/gbcs/gbcs.xsd")
// val schemaUrl = GBCS.CONFIGURATION_SCHEMA_URL
val dbf = Xml.newDocumentBuilderFactory(null) val dbf = Xml.newDocumentBuilderFactory(null)
// dbf.schema = Xml.getSchema(this::class.java.module.getResourceAsStream("/net/woggioni/gbcs/gbcs.xsd"))
// dbf.schema = Xml.getSchema(schemaUrl)
val db = dbf.newDocumentBuilder() val db = dbf.newDocumentBuilder()
val doc = Files.newInputStream(configurationFile).use(db::parse) val doc = Files.newInputStream(configurationFile).use(db::parse)
return Parser.parse(doc) return Parser.parse(doc)
} }
@JvmStatic fun dumpConfiguration(conf : Configuration, outputStream: OutputStream) {
fun main(args: Array<String>) { Xml.write(Serializer.serialize(conf), outputStream)
ClasspathUrlStreamHandlerFactoryProvider.install()
val configuration = loadConfiguration(args)
log.debug {
ByteArrayOutputStream().also {
Xml.write(Serializer.serialize(configuration), it)
}.let {
"Server configuration:\n${String(it.toByteArray())}"
}
}
GradleBuildCacheServer(configuration).run().use {
}
}
}
}
object GraalNativeImageConfiguration {
@JvmStatic
fun main(args: Array<String>) {
val conf = GradleBuildCacheServer.loadConfiguration(args)
GradleBuildCacheServer(conf).run().use {
Thread.sleep(3000)
it.shutdown()
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs package net.woggioni.gbcs.auth
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener import io.netty.channel.ChannelFutureListener
@@ -12,18 +12,12 @@ import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.util.ReferenceCountUtil import io.netty.util.ReferenceCountUtil
import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.api.Role
import java.security.SecureRandom
import java.security.spec.KeySpec
import java.util.Base64
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
abstract class AbstractNettyHttpAuthenticator(private val authorizer : Authorizer) abstract class AbstractNettyHttpAuthenticator(private val authorizer : Authorizer)
: ChannelInboundHandlerAdapter() { : ChannelInboundHandlerAdapter() {
companion object { companion object {
private const val KEY_LENGTH = 256
private val AUTHENTICATION_FAILED: FullHttpResponse = DefaultFullHttpResponse( private val AUTHENTICATION_FAILED: FullHttpResponse = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, Unpooled.EMPTY_BUFFER).apply { HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, Unpooled.EMPTY_BUFFER).apply {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0" headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
@@ -33,42 +27,6 @@ abstract class AbstractNettyHttpAuthenticator(private val authorizer : Authorize
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER).apply { HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN, Unpooled.EMPTY_BUFFER).apply {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0" headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
} }
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
val result = ByteArray(arr1.size + arr2.size)
var j = 0
for(element in arr1) {
result[j] = element
j += 1
}
for(element in arr2) {
result[j] = element
j += 1
}
return result
}
fun hashPassword(password : String, salt : String? = null) : String {
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
val result = ByteArray(16)
nextBytes(result)
result
}
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, 10, KEY_LENGTH)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
val hash = factory.generateSecret(spec).encoded
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
}
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(passwordHash)
val hash = ByteArray(KEY_LENGTH / 8)
val salt = ByteArray(decoded.size - KEY_LENGTH / 8)
System.arraycopy(decoded, 0, hash, 0, hash.size)
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
return hash to salt
}
} }

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs package net.woggioni.gbcs.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 package net.woggioni.gbcs.auth
import java.security.KeyStore import java.security.KeyStore
import java.security.cert.CertPathValidator import java.security.cert.CertPathValidator

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs package net.woggioni.gbcs.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,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"
xs:schemaLocation="urn:net.woggioni.gbcs classpath:net/woggioni/gbcs/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/schema/gbcs.xsd">
<bind host="127.0.0.1" port="11443"/> <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>
<none/> <none/>

View File

@@ -0,0 +1,30 @@
package net.woggioni.gbcs.utils;
import net.woggioni.jwo.JWO;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
public class NetworkUtils {
private static final int MAX_ATTEMPTS = 50;
public static int getFreePort() {
int count = 0;
while(count < MAX_ATTEMPTS) {
try (ServerSocket serverSocket = new ServerSocket(0, 50, InetAddress.getLocalHost())) {
final var candidate = serverSocket.getLocalPort();
if (candidate > 0) {
return candidate;
} else {
JWO.newThrowable(RuntimeException.class, "Got invalid port number: %d", candidate);
throw new RuntimeException("Error trying to find an open port");
}
} catch (IOException ignored) {
++count;
}
}
throw new RuntimeException("Error trying to find an open port");
}
}

View File

@@ -1,15 +1,17 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.AbstractNettyHttpAuthenticator.Companion.hashPassword
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.PasswordSecurity.hashPassword
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.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.io.IOException
import java.net.ServerSocket import java.net.ServerSocket
import java.net.URI import java.net.URI
import java.net.http.HttpClient import java.net.http.HttpClient
@@ -41,7 +43,7 @@ class BasicAuthServerTest : AbstractServerTest() {
val writersGroup = Configuration.Group("writers", setOf(Role.Writer)) val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
ServerSocket(0).localPort + 1, NetworkUtils.getFreePort(),
serverPath, serverPath,
listOf( listOf(
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)), Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)),

View File

@@ -1,10 +1,10 @@
package net.woggioni.gbcs.test package net.woggioni.gbcs.test
import net.woggioni.gbcs.base.GbcsUrlStreamHandlerFactory
import net.woggioni.gbcs.base.GBCS.toUrl import net.woggioni.gbcs.base.GBCS.toUrl
import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.configuration.Parser import net.woggioni.gbcs.configuration.Parser
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
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
@@ -22,7 +22,7 @@ class ConfigurationTest {
) )
@ParameterizedTest @ParameterizedTest
fun test(configurationUrl: String, @TempDir testDir: Path) { fun test(configurationUrl: String, @TempDir testDir: Path) {
ClasspathUrlStreamHandlerFactoryProvider.install() GbcsUrlStreamHandlerFactory.install()
val doc = Xml.parseXml(configurationUrl.toUrl()) val doc = Xml.parseXml(configurationUrl.toUrl())
val cfg = Parser.parse(doc) val cfg = Parser.parse(doc)
val configFile = testDir.resolve("gbcs.xml") val configFile = testDir.resolve("gbcs.xml")

View File

@@ -5,6 +5,7 @@ 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.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.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
@@ -32,7 +33,7 @@ class NoAuthServerTest : AbstractServerTest() {
this.cacheDir = testDir.resolve("cache") this.cacheDir = testDir.resolve("cache")
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
ServerSocket(0).localPort + 1, NetworkUtils.getFreePort(),
serverPath, serverPath,
emptyMap(), emptyMap(),
emptyMap(), emptyMap(),

View File

@@ -8,6 +8,7 @@ import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.utils.CertificateUtils import net.woggioni.gbcs.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials 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
@@ -146,7 +147,7 @@ class TlsServerTest : AbstractServerTest() {
createKeyStoreAndTrustStore() createKeyStoreAndTrustStore()
cfg = Configuration( cfg = Configuration(
"127.0.0.1", "127.0.0.1",
ServerSocket(0).localPort + 1, NetworkUtils.getFreePort(),
serverPath, serverPath,
users.asSequence().map { it.name to it }.toMap(), users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(), sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
@@ -227,7 +228,6 @@ class TlsServerTest : AbstractServerTest() {
@Test @Test
@Order(3) @Order(3)
fun getAsAWriterUser() { fun getAsAWriterUser() {
val (key, _) = keyValuePair val (key, _) = keyValuePair
val user = cfg.users.values.find { val user = cfg.users.values.find {
Role.Writer in it.roles Role.Writer in it.roles
@@ -245,7 +245,6 @@ class TlsServerTest : AbstractServerTest() {
@Test @Test
@Order(4) @Order(4)
fun putAsAWriterUser() { fun putAsAWriterUser() {
val (key, value) = keyValuePair val (key, value) = keyValuePair
val user = cfg.users.values.find { val user = cfg.users.values.find {
Role.Writer in it.roles Role.Writer in it.roles

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"
xs:schemaLocation="urn:net.woggioni.gbcs classpath:net/woggioni/gbcs/schema/gbcs.xsd"> xs:schemaLocation="urn:net.woggioni.gbcs jpms://net.woggioni.gbcs/net/woggioni/gbcs/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

@@ -2,7 +2,7 @@
<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"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached" xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached"
xs:schemaLocation="urn:net.woggioni.gbcs classpath:net/woggioni/gbcs/schema/gbcs.xsd urn:net.woggioni.gbcs-memcached classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd"> 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">
<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" compression-mode="gzip" digest="SHA-256"> <cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="101325" compression-mode="gzip" digest="SHA-256">
<server host="127.0.0.1" port="11211"/> <server host="127.0.0.1" port="11211"/>