diff --git a/.gitignore b/.gitignore index 1b6985c..86dc90f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # Ignore Gradle build output directory build + +gbcs-cli/native-image/*.json diff --git a/Dockerfile b/Dockerfile index 26f791e..b91d6be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,11 +9,11 @@ WORKDIR /home/ubuntu RUN mkdir gbcs WORKDIR /home/ubuntu/gbcs -COPY --chown=ubuntu:users native-image native-image COPY --chown=ubuntu:users .git .git COPY --chown=ubuntu:users gbcs-base gbcs-base COPY --chown=ubuntu:users gbcs-api gbcs-api 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 settings.gradle settings.gradle COPY --chown=ubuntu:users build.gradle build.gradle @@ -31,11 +31,11 @@ USER luser WORKDIR /home/luser 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"] 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 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 diff --git a/build.gradle b/build.gradle index c2b2651..4421797 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,11 @@ plugins { id 'java-library' - id 'jvm-test-suite' alias catalog.plugins.kotlin.jvm - alias catalog.plugins.envelope alias catalog.plugins.sambal alias catalog.plugins.lombok - 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 @@ -41,6 +34,11 @@ allprojects { } pluginManager.withPlugin('java-library') { + + ext { + jpmsModuleName = project.group + '.' + project.name.replace('-', '.') + } + java { withSourcesJar() modularity.inferModulePath = true @@ -58,8 +56,19 @@ allprojects { modularity.inferModulePath = true options.release = 21 } + + tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { + options.compilerArgumentProviders << new CommandLineArgumentProvider() { + @Override + Iterable asArguments() { + return ['--patch-module', project.jpmsModuleName + '=' + project.sourceSets.main.output.asPath] + } + } + options.javaModuleVersion = version + } } + pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) { tasks.withType(KotlinCompile.class) { compilerOptions.jvmTarget = JvmTarget.JVM_21 @@ -92,34 +101,23 @@ allprojects { } } } -} -Property 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"] + tasks.withType(AbstractArchiveTask.class) { + archiveVersion = project.version + } } dependencies { implementation catalog.jwo implementation catalog.slf4j.api implementation catalog.netty.codec.http + implementation catalog.netty.codec.http2 - implementation project('gbcs-base') - implementation project('gbcs-api') + api project('gbcs-base') + api project('gbcs-api') // runtimeOnly catalog.slf4j.jdk14 - runtimeOnly catalog.logback.classic + testRuntimeOnly catalog.logback.classic testImplementation catalog.bcprov.jdk18on testImplementation catalog.bcpkix.jdk18on @@ -130,32 +128,10 @@ dependencies { testRuntimeOnly project("gbcs-memcached") } -Provider 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 { publications { maven(MavenPublication) { - artifact envelopeJarArtifact + from(components["java"]) } } } diff --git a/gbcs-api/build.gradle b/gbcs-api/build.gradle index e40611f..970f25d 100644 --- a/gbcs-api/build.gradle +++ b/gbcs-api/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java-library' + id 'maven-publish' alias catalog.plugins.lombok } @@ -9,3 +10,11 @@ dependencies { tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { options.javaModuleVersion = version } + +publishing { + publications { + maven(MavenPublication) { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/gbcs-base/build.gradle b/gbcs-base/build.gradle index 3445dfa..917f3d4 100644 --- a/gbcs-base/build.gradle +++ b/gbcs-base/build.gradle @@ -3,12 +3,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id 'java-library' + id 'maven-publish' alias catalog.plugins.kotlin.jvm } dependencies { compileOnly project(':gbcs-api') compileOnly catalog.slf4j.api + api catalog.jwo } tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { @@ -19,3 +21,11 @@ tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { tasks.named("compileKotlin", KotlinCompile.class) { compilerOptions.jvmTarget = JvmTarget.JVM_21 } + +publishing { + publications { + maven(MavenPublication) { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/gbcs-base/src/main/java/module-info.java b/gbcs-base/src/main/java/module-info.java index 2fa3505..7ebf117 100644 --- a/gbcs-base/src/main/java/module-info.java +++ b/gbcs-base/src/main/java/module-info.java @@ -3,6 +3,7 @@ module net.woggioni.gbcs.base { requires java.logging; requires org.slf4j; requires kotlin.stdlib; + requires net.woggioni.jwo; exports net.woggioni.gbcs.base; } \ No newline at end of file diff --git a/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/ClasspathUrlStreamHandlerFactoryProvider.kt b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/ClasspathUrlStreamHandlerFactoryProvider.kt new file mode 100644 index 0000000..826ac67 --- /dev/null +++ b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/ClasspathUrlStreamHandlerFactoryProvider.kt @@ -0,0 +1,79 @@ +package net.woggioni.gbcs.base + +import java.io.InputStream +import java.net.URL +import java.net.URLConnection +import java.net.URLStreamHandler +import java.net.URLStreamHandlerFactory +import java.util.concurrent.atomic.AtomicBoolean +import java.util.stream.Collectors + + +class ClasspathUrlStreamHandlerFactoryProvider : URLStreamHandlerFactory { + private class Handler(private val classLoader: ClassLoader = ClasspathUrlStreamHandlerFactoryProvider::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]!! + ModuleResourceURLConnection( + u, + modules + ) + } + ?: classLoader.getResource(u.path)?.let(URL::openConnection) + } + } + + override fun createURLStreamHandler(protocol: String): URLStreamHandler? { + return when (protocol) { + "classpath" -> Handler() + else -> null + } + } + + private class ModuleResourceURLConnection(url: URL?, private val modules: List) : + 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(ClasspathUrlStreamHandlerFactoryProvider()) + } + } + + private val packageMap: Map> by lazy { + ClasspathUrlStreamHandlerFactoryProvider::class.java.module.layer + .modules() + .stream() + .flatMap { m: Module -> + m.packages.stream() + .map { p: String -> p to m } + } + .collect( + Collectors.groupingBy( + Pair::first, + Collectors.mapping( + Pair::second, + Collectors.toUnmodifiableList() + ) + ) + ) + } + } +} \ No newline at end of file diff --git a/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/PasswordSecurity.kt b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/PasswordSecurity.kt new file mode 100644 index 0000000..a40579b --- /dev/null +++ b/gbcs-base/src/main/kotlin/net/woggioni/gbcs/base/PasswordSecurity.kt @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/gbcs-cli/build.gradle b/gbcs-cli/build.gradle new file mode 100644 index 0000000..a5c4794 --- /dev/null +++ b/gbcs-cli/build.gradle @@ -0,0 +1,75 @@ +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 mainClassName = objects.property(String.class) +mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli') + +tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { + options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs.cli=' + project.sourceSets.main.output.asPath + options.javaModuleVersion = version + 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 project(':gbcs-base') +// implementation project(':gbcs-api') + implementation rootProject + +// runtimeOnly catalog.slf4j.jdk14 + runtimeOnly catalog.logback.classic +} + +Provider 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' +} + +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 { + publications { + maven(MavenPublication) { + artifact envelopeJarArtifact + } + } +} + diff --git a/native-image/native-image.properties b/gbcs-cli/native-image/native-image.properties similarity index 100% rename from native-image/native-image.properties rename to gbcs-cli/native-image/native-image.properties diff --git a/gbcs-cli/src/main/java/module-info.java b/gbcs-cli/src/main/java/module-info.java new file mode 100644 index 0000000..f105f90 --- /dev/null +++ b/gbcs-cli/src/main/java/module-info.java @@ -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; +} \ No newline at end of file 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 new file mode 100644 index 0000000..5bf5ece --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/GradleBuildCacheServerCli.kt @@ -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.ClasspathUrlStreamHandlerFactoryProvider +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 + ClasspathUrlStreamHandlerFactoryProvider.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 { + } + } +} \ No newline at end of file diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/AbstractVersionProvider.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/AbstractVersionProvider.kt new file mode 100644 index 0000000..5dbf3d7 --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/AbstractVersionProvider.kt @@ -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 { + return if (version.endsWith("-SNAPSHOT")) { + arrayOf(version, vcsHash) + } else { + arrayOf(version) + } + } +} diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/GbcsCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/GbcsCommand.kt new file mode 100644 index 0000000..bb0f905 --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/GbcsCommand.kt @@ -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 +} \ No newline at end of file diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt new file mode 100644 index 0000000..7a39f8c --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/commands/PasswordHashCommand.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/converters/OutputStreamConverter.kt b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/converters/OutputStreamConverter.kt new file mode 100644 index 0000000..ae3fa2c --- /dev/null +++ b/gbcs-cli/src/main/kotlin/net/woggioni/gbcs/cli/impl/converters/OutputStreamConverter.kt @@ -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 { + override fun convert(value: String): OutputStream { + return Files.newOutputStream(Paths.get(value)) + } +} \ No newline at end of file diff --git a/src/main/resources/net/woggioni/gbcs/logback.xml b/gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml similarity index 95% rename from src/main/resources/net/woggioni/gbcs/logback.xml rename to gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml index cc0ff2f..76a457a 100644 --- a/src/main/resources/net/woggioni/gbcs/logback.xml +++ b/gbcs-cli/src/main/resources/net/woggioni/gbcs/cli/logback.xml @@ -6,7 +6,7 @@ - System.out + System.err %d [%highlight(%-5level)] \(%thread\) %logger{36} -%kvp- %msg %n diff --git a/gradle.properties b/gradle.properties index 2caf664..9c1ad5d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ org.gradle.configuration-cache=false org.gradle.parallel=true -org.gradle.caching=true +org.gradle.caching=false 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 diff --git a/settings.gradle b/settings.gradle index a68cb7c..9122d9b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,5 +29,6 @@ rootProject.name = 'gbcs' include 'gbcs-api' include 'gbcs-base' include 'gbcs-memcached' +include 'gbcs-cli' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 30f594d..3c1d879 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,7 +2,7 @@ import net.woggioni.gbcs.api.CacheProvider; import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider; import net.woggioni.gbcs.cache.FileSystemCacheProvider; -open module net.woggioni.gbcs { +module net.woggioni.gbcs { requires java.sql; requires java.xml; requires java.logging; @@ -11,6 +11,7 @@ open module net.woggioni.gbcs { requires io.netty.buffer; requires io.netty.transport; requires io.netty.codec.http; + requires io.netty.codec.http2; requires io.netty.common; requires io.netty.handler; requires io.netty.codec; @@ -19,9 +20,12 @@ open module net.woggioni.gbcs { requires net.woggioni.gbcs.base; requires net.woggioni.gbcs.api; - provides java.net.URLStreamHandlerFactory with ClasspathUrlStreamHandlerFactoryProvider; - uses java.net.URLStreamHandlerFactory; - uses CacheProvider; + exports net.woggioni.gbcs; + opens net.woggioni.gbcs; + opens net.woggioni.gbcs.schema; + uses java.net.URLStreamHandlerFactory; + provides java.net.URLStreamHandlerFactory with ClasspathUrlStreamHandlerFactoryProvider; + uses CacheProvider; provides CacheProvider with FileSystemCacheProvider; } \ No newline at end of file diff --git a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt index ce1b532..1711819 100644 --- a/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/GradleBuildCacheServer.kt @@ -34,6 +34,9 @@ import io.netty.handler.codec.http.HttpServerCodec import io.netty.handler.codec.http.HttpUtil import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.LastHttpContent +import io.netty.handler.codec.http2.Http2FrameCodecBuilder +import io.netty.handler.ssl.ApplicationProtocolNames +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler import io.netty.handler.ssl.ClientAuth import io.netty.handler.ssl.SslContext import io.netty.handler.ssl.SslContextBuilder @@ -46,7 +49,13 @@ import net.woggioni.gbcs.api.Cache import net.woggioni.gbcs.api.Configuration import net.woggioni.gbcs.api.Role 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.PasswordSecurity.decodePasswordHash +import net.woggioni.gbcs.base.PasswordSecurity.hashPassword import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.base.contextLogger import net.woggioni.gbcs.base.debug @@ -58,6 +67,7 @@ import net.woggioni.jwo.Application import net.woggioni.jwo.JWO import net.woggioni.jwo.Tuple2 import java.io.ByteArrayOutputStream +import java.io.OutputStream import java.net.InetSocketAddress import java.net.URL import java.net.URLStreamHandlerFactory @@ -213,6 +223,22 @@ class GradleBuildCacheServer(private val cfg: Configuration) { companion object { + private fun getServerAPNHandler(): ApplicationProtocolNegotiationHandler { + val serverAPNHandler: ApplicationProtocolNegotiationHandler = + object : ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) { + override fun configurePipeline(ctx: ChannelHandlerContext, protocol: String) { + if (ApplicationProtocolNames.HTTP_2 == protocol) { + ctx.pipeline().addLast( + Http2FrameCodecBuilder.forServer().build() + ) + return + } + throw IllegalStateException("Protocol: $protocol not supported") + } + } + return serverAPNHandler + } + fun loadKeystore(file: Path, password: String?): KeyStore { val ext = JWO.splitExtension(file) .map(Tuple2::get_2) @@ -273,7 +299,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) { } if (sslContext != null) { val sslHandler = sslContext.newHandler(ch.alloc()) - pipeline.addLast(sslHandler) if (auth is Configuration.ClientCertificateAuthentication) { val roleAuthorizer = RoleAuthorizer() @@ -285,6 +310,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) { ) } } +// pipeline.addLast(getServerAPNHandler()) pipeline.addLast(HttpServerCodec()) pipeline.addLast(HttpChunkContentCompressor(1024)) pipeline.addLast(ChunkedWriteHandler()) @@ -537,6 +563,17 @@ class GradleBuildCacheServer(private val cfg: Configuration) { resetCachedUrlHandlers() } + fun loadConfiguration(configurationFile: Path): Configuration { + val dbf = Xml.newDocumentBuilderFactory(null) + val db = dbf.newDocumentBuilder() + val doc = Files.newInputStream(configurationFile).use(db::parse) + return Parser.parse(doc) + } + + fun dumpConfiguration(conf : Configuration, outputStream: OutputStream) { + Xml.write(Serializer.serialize(conf), outputStream) + } + fun loadConfiguration(args: Array): Configuration { // Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader val app = Application.builder("gbcs") diff --git a/src/main/kotlin/net/woggioni/gbcs/Authenticator.kt b/src/main/kotlin/net/woggioni/gbcs/auth/Authenticator.kt similarity index 56% rename from src/main/kotlin/net/woggioni/gbcs/Authenticator.kt rename to src/main/kotlin/net/woggioni/gbcs/auth/Authenticator.kt index 1eca97b..62d5c98 100644 --- a/src/main/kotlin/net/woggioni/gbcs/Authenticator.kt +++ b/src/main/kotlin/net/woggioni/gbcs/auth/Authenticator.kt @@ -1,4 +1,4 @@ -package net.woggioni.gbcs +package net.woggioni.gbcs.auth import io.netty.buffer.Unpooled 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.util.ReferenceCountUtil 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) : ChannelInboundHandlerAdapter() { companion object { - private const val KEY_LENGTH = 256 private val AUTHENTICATION_FAILED: FullHttpResponse = DefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, Unpooled.EMPTY_BUFFER).apply { 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 { 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 { - 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 - } } diff --git a/src/main/kotlin/net/woggioni/gbcs/Authorizer.kt b/src/main/kotlin/net/woggioni/gbcs/auth/Authorizer.kt similarity index 85% rename from src/main/kotlin/net/woggioni/gbcs/Authorizer.kt rename to src/main/kotlin/net/woggioni/gbcs/auth/Authorizer.kt index d26f123..6eb5bab 100644 --- a/src/main/kotlin/net/woggioni/gbcs/Authorizer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/auth/Authorizer.kt @@ -1,4 +1,4 @@ -package net.woggioni.gbcs +package net.woggioni.gbcs.auth import io.netty.handler.codec.http.HttpRequest import net.woggioni.gbcs.api.Role diff --git a/src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt b/src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt similarity index 99% rename from src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt rename to src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt index 18afac7..8af44a7 100644 --- a/src/main/kotlin/net/woggioni/gbcs/ClientCertificateValidator.kt +++ b/src/main/kotlin/net/woggioni/gbcs/auth/ClientCertificateValidator.kt @@ -1,4 +1,4 @@ -package net.woggioni.gbcs +package net.woggioni.gbcs.auth import java.security.KeyStore import java.security.cert.CertPathValidator diff --git a/src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt b/src/main/kotlin/net/woggioni/gbcs/auth/UserAuthorizer.kt similarity index 95% rename from src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt rename to src/main/kotlin/net/woggioni/gbcs/auth/UserAuthorizer.kt index b695a79..9be8e8f 100644 --- a/src/main/kotlin/net/woggioni/gbcs/UserAuthorizer.kt +++ b/src/main/kotlin/net/woggioni/gbcs/auth/UserAuthorizer.kt @@ -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.HttpRequest diff --git a/src/main/resources/net/woggioni/gbcs/gbcs-default.xml b/src/main/resources/net/woggioni/gbcs/gbcs-default.xml index 369b9e3..38006f9 100644 --- a/src/main/resources/net/woggioni/gbcs/gbcs-default.xml +++ b/src/main/resources/net/woggioni/gbcs/gbcs-default.xml @@ -2,7 +2,7 @@ - + diff --git a/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt b/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt index bbea4b0..155452b 100644 --- a/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt +++ b/src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt @@ -1,7 +1,7 @@ package net.woggioni.gbcs.test import io.netty.handler.codec.http.HttpResponseStatus -import net.woggioni.gbcs.AbstractNettyHttpAuthenticator.Companion.hashPassword +import net.woggioni.gbcs.auth.AbstractNettyHttpAuthenticator.Companion.hashPassword import net.woggioni.gbcs.api.Role import net.woggioni.gbcs.base.Xml import net.woggioni.gbcs.api.Configuration