temporary commit

This commit is contained in:
2025-01-08 23:17:43 +08:00
parent 688a196a52
commit 0fdb37fb54
74 changed files with 3302 additions and 675 deletions

View File

@@ -9,9 +9,53 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: graalvm
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Execute Gradle build
env:
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
run: ./gradlew build publish
run: ./gradlew build publish
build-docker:
name: "Build Docker images"
runs-on: hostinger
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
with:
driver: docker-container
-
name: Login to Gitea container registry
uses: docker/login-action@v3
with:
registry: gitea.woggioni.net
username: woggioni
password: ${{ secrets.PUBLISHER_TOKEN }}
-
name: Build and push gbcs images
uses: docker/build-push-action@v6
with:
push: true
pull: true
tags: |
"gitea.woggioni.net/woggioni/gbcs:slim"
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/gbcs:buildx
target: release
-
name: Build and push gbcs memcached image
uses: docker/build-push-action@v6
with:
push: true
pull: true
tags: |
"gitea.woggioni.net/woggioni/gbcs:latest"
"gitea.woggioni.net/woggioni/gbcs:memcached"
cache-from: type=registry,ref=gitea.woggioni.net/woggioni/gbcs:buildx
cache-to: type=registry,mode=max,compression=zstd,image-manifest=true,oci-mediatypes=true,ref=gitea.woggioni.net/woggioni/gbcs:buildx
target: release-memcached

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
FROM container-registry.oracle.com/graalvm/native-image:21 AS oracle
FROM ubuntu:24.04 AS build
COPY --from=oracle /usr/lib64/graalvm/ /usr/lib64/graalvm/
ENV JAVA_HOME=/usr/lib64/graalvm/graalvm-java21
USER ubuntu
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 src src
COPY --chown=ubuntu:users settings.gradle settings.gradle
COPY --chown=ubuntu:users build.gradle build.gradle
COPY --chown=ubuntu:users gradle.properties gradle.properties
COPY --chown=ubuntu:users gradle gradle
COPY --chown=ubuntu:users gradlew gradlew
RUN --mount=type=cache,target=/home/ubuntu/.gradle,uid=1000,gid=1000 ./gradlew --no-daemon assemble
FROM alpine:latest AS base-release
RUN --mount=type=cache,target=/var/cache/apk apk update
RUN --mount=type=cache,target=/var/cache/apk apk add openjdk21-jre
RUN adduser -D luser
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
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 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
WORKDIR /home/luser
ENTRYPOINT ["java", "-jar", "/home/luser/gbcs.jar"]
FROM release-memcached as compose
COPY --chown=luser:luser conf/gbcs-memcached.xml /home/luser/.config/gbcs/gbcs.xml

View File

@@ -1,40 +1,198 @@
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
group = 'net.woggioni'
version = project.currentTag ?: "${getProperty('gbcs.version')}.${project.gitRevision[0..10]}"
allprojects {
group = 'net.woggioni'
if(project.currentTag.isPresent()) {
version = project.currentTag
} else {
version = project.gitRevision.map { gitRevision ->
"${getProperty('gbcs.version')}.${gitRevision[0..10]}"
}
}
repositories {
maven {
url = getProperty('gitea.maven.url')
content {
includeModule 'net.woggioni', 'jwo'
includeModule 'net.woggioni', 'xmemcached'
includeGroup 'com.lys'
}
}
mavenCentral()
}
pluginManager.withPlugin('java-library') {
java {
withSourcesJar()
modularity.inferModulePath = true
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ORACLE
}
}
test {
useJUnitPlatform()
}
tasks.withType(JavaCompile) {
modularity.inferModulePath = true
options.release = 21
}
}
pluginManager.withPlugin(catalog.plugins.kotlin.jvm.get().pluginId) {
tasks.withType(KotlinCompile.class) {
compilerOptions.jvmTarget = JvmTarget.JVM_21
}
}
pluginManager.withPlugin(catalog.plugins.lombok.get().pluginId) {
lombok {
version = catalog.versions.lombok
}
}
pluginManager.withPlugin('maven-publish') {
publishing {
repositories {
maven {
name = "Gitea"
url = uri(getProperty('gitea.maven.url'))
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "token ${System.getenv()["PUBLISHER_TOKEN"]}"
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
}
}
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
}
//tasks.named(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME, JavaCompile) {
// options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs.test=' + project.sourceSets.test.output.asPath
// classpath = configurations.testCompileClasspath + files(tasks.jar.archiveFile)
// modularity.inferModulePath = true
// javaModuleDetector
//}
//tasks.named(JavaPlugin.TEST_TASK_NAME, JavaForkOptions) {
// classpath = configurations.testRuntimeClasspath + project.files(tasks.jar.archiveFile) + project.sourceSets.test.output
// jvmArgumentProviders << new CommandLineArgumentProvider() {
// @CompileClasspath
// def kotlinClassesMain = kotlin.sourceSets.main.collect { it.kotlin.classesDirectory }
//
// @CompileClasspath
// def kotlinClassesTest = kotlin.sourceSets.main.collect { it.kotlin.classesDirectory }
// @Override
// Iterable<String> asArguments() {
// return [
// "--patch-module",
// 'net.woggioni.gbcs=' + kotlinClassesMain.collect { it.get().asFile.absolutePath },
// "--patch-module",
// 'net.woggioni.gbcs.test=' + project.sourceSets.test.output.asPath,
// ]
// }
// }
//}
//configurations {
// integrationTestImplementation {
// attributes {
// attribute(LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements.class, JAR))
// }
// }
//
// integrationTestCompileClasspath {
// attributes {
// attribute(LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements.class, JAR))
// }
// }
//}
//
envelopeJar {
mainModule = 'net.woggioni.gbcs'
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer'
mainClass = mainClassName
extraClasspath = ["plugins"]
}
repositories {
maven {
url = getProperty('gitea.maven.url')
content {
includeModule 'net.woggioni', 'jwo'
includeGroup 'com.lys'
}
}
mavenCentral()
}
//
//testing {
// suites {
// test {
// useJUnitJupiter(catalog.versions.junit.jupiter.get())
// }
//
// integrationTest(JvmTestSuite) {
// dependencies {
// implementation project()
// implementation catalog.bcprov.jdk18on
// implementation catalog.bcpkix.jdk18on
// annotationProcessor catalog.lombok
// compileOnly catalog.lombok
// implementation project('gbcs-base')
// implementation project('gbcs-api')
//
// runtimeOnly project("gbcs-memcached")
// }
//
// targets {
// all {
// testTask.configure {
// shouldRunAfter(test)
// }
// }
// }
// }
// }
//}
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
implementation catalog.netty.codec.http
implementation project('gbcs-base')
implementation project('gbcs-api')
// runtimeOnly catalog.slf4j.jdk14
runtimeOnly catalog.logback.classic
@@ -43,40 +201,14 @@ dependencies {
testImplementation catalog.junit.jupiter.api
testImplementation catalog.junit.jupiter.params
testRuntimeOnly catalog.junit.jupiter.engine
}
java {
withSourcesJar()
modularity.inferModulePath = true
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
test {
useJUnitPlatform()
}
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs=' + project.sourceSets.main.output.asPath
options.javaModuleVersion = provider { version }
options.javaModuleMainClass = envelopeJar.mainClass
}
tasks.withType(JavaCompile) {
modularity.inferModulePath = true
options.release = 21
}
tasks.named("compileKotlin", KotlinCompile.class) {
compilerOptions.jvmTarget = JvmTarget.JVM_21
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:logback.xml'
systemProperties['logback.configurationFile'] = 'classpath:net/woggioni/gbcs/logback.xml'
}
@@ -85,25 +217,37 @@ def envelopeJarArtifact = artifacts.add('archives', envelopeJarTaskProvider.get(
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 {
repositories {
maven {
name = "Gitea"
url = uri(getProperty('gitea.maven.url'))
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "token ${System.getenv()["PUBLISHER_TOKEN"]}"
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
publications {
maven(MavenPublication) {
artifact envelopeJarArtifact
}
}
}
}
//tasks.named('check') {
// dependsOn(testing.suites.integrationTest)
//}
//
//tasks.named("integrationTest", JavaForkOptions) {
// jvmArgumentProviders << new CommandLineArgumentProvider() {
// @Override
// Iterable<String> asArguments() {
// return [
// "--patch-module",
// 'net.woggioni.gbcs.test=' + project.sourceSets.integrationTest.output.asPath,
// ]
// }
// }
//}

13
conf/gbcs-memcached.xml Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs"
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">
<bind host="0.0.0.0" port="13080" />
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" max-size="16777216" compression-mode="zip">
<server host="memcached" port="11211"/>
</cache>
<authentication>
<none/>
</authentication>
</gbcs:server>

37
docker-compose.yml Normal file
View File

@@ -0,0 +1,37 @@
networks:
default:
external: false
ipam:
driver: default
config:
- subnet: 172.118.0.0/16
ip_range: 172.118.0.0/16
gateway: 172.118.0.254
services:
gbcs:
build:
context: .
target: compose
container_name: gbcs
restart: unless-stopped
ports:
- "127.0.0.1:8080:13080"
- "[::1]:8080:13080"
depends_on:
memcached:
condition: service_started
deploy:
resources:
limits:
cpus: "2.00"
memory: 256M
memcached:
image: memcached
container_name: memcached
restart: unless-stopped
command: -I 64m -m 900m
deploy:
resources:
limits:
cpus: "1.00"
memory: 1G

11
gbcs-api/build.gradle Normal file
View File

@@ -0,0 +1,11 @@
plugins {
id 'java-library'
alias catalog.plugins.lombok
}
dependencies {
}
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleVersion = version
}

View File

@@ -0,0 +1,6 @@
module net.woggioni.gbcs.api {
requires static lombok;
requires java.xml;
exports net.woggioni.gbcs.api;
exports net.woggioni.gbcs.api.exception;
}

View File

@@ -0,0 +1,11 @@
package net.woggioni.gbcs.api;
import net.woggioni.gbcs.api.exception.ContentTooLargeException;
import java.nio.channels.ReadableByteChannel;
public interface Cache extends AutoCloseable {
ReadableByteChannel get(String key);
void put(String key, byte[] content) throws ContentTooLargeException;
}

View File

@@ -0,0 +1,17 @@
package net.woggioni.gbcs.api;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
public interface CacheProvider<T extends Configuration.Cache> {
String getXmlSchemaLocation();
String getXmlNamespace();
String getXmlType();
T deserialize(Element parent);
Element serialize(Document doc, T cache);
}

View File

@@ -0,0 +1,138 @@
package net.woggioni.gbcs.api;
import lombok.Value;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Value
public class Configuration {
String host;
int port;
String serverPath;
Map<String, User> users;
Map<String, Group> groups;
Cache cache;
Authentication authentication;
Tls tls;
boolean useVirtualThread;
@Value
public static class Group {
String name;
Set<Role> roles;
@Override
public int hashCode() {
return name.hashCode();
}
}
@Value
public static class User {
String name;
String password;
Set<Group> groups;
@Override
public int hashCode() {
return name.hashCode();
}
public Set<Role> getRoles() {
return groups.stream()
.flatMap(group -> group.getRoles().stream())
.collect(Collectors.toSet());
}
}
@FunctionalInterface
public interface UserExtractor {
User extract(X509Certificate cert);
}
@FunctionalInterface
public interface GroupExtractor {
Group extract(X509Certificate cert);
}
@Value
public static class Tls {
KeyStore keyStore;
TrustStore trustStore;
boolean verifyClients;
}
@Value
public static class KeyStore {
Path file;
String password;
String keyAlias;
String keyPassword;
}
@Value
public static class TrustStore {
Path file;
String password;
boolean checkCertificateStatus;
}
@Value
public static class TlsCertificateExtractor {
String rdnType;
String pattern;
}
public interface Authentication {}
public static class BasicAuthentication implements Authentication {}
@Value
public static class ClientCertificateAuthentication implements Authentication {
TlsCertificateExtractor userExtractor;
TlsCertificateExtractor groupExtractor;
}
public interface Cache {
net.woggioni.gbcs.api.Cache materialize();
String getNamespaceURI();
String getTypeName();
}
// @Value
// public static class FileSystemCache implements Cache {
// Path root;
// Duration maxAge;
// }
public static Configuration of(
String host,
int port,
String serverPath,
Map<String, User> users,
Map<String, Group> groups,
Cache cache,
Authentication authentication,
Tls tls,
boolean useVirtualThread
) {
return new Configuration(
host,
port,
serverPath != null && !serverPath.isEmpty() && !serverPath.equals("/") ? serverPath : null,
users,
groups,
cache,
authentication,
tls,
useVirtualThread
);
}
}

View File

@@ -0,0 +1,237 @@
package net.woggioni.gbcs.api;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
public class ConfigurationParser {
public static Configuration parse(Document document) {
Element root = document.getDocumentElement();
Configuration.Cache cache = null;
String host = "127.0.0.1";
int port = 11080;
Map<String, Configuration.User> users = Collections.emptyMap();
Map<String, Configuration.Group> groups = Collections.emptyMap();
Configuration.Tls tls = null;
String serverPath = root.getAttribute("path");
boolean useVirtualThread = !root.getAttribute("useVirtualThreads").isEmpty() &&
Boolean.parseBoolean(root.getAttribute("useVirtualThreads"));
Configuration.Authentication authentication = null;
for (Node child : iterableOf(root)) {
switch (child.getNodeName()) {
case "authorization":
for (Node gchild : iterableOf((Element) child)) {
switch (gchild.getNodeName()) {
case "users":
users = parseUsers((Element) gchild);
break;
case "groups":
Map.Entry<Map<String, Configuration.User>, Map<String, Configuration.Group>> pair = parseGroups((Element) gchild, users);
users = pair.getKey();
groups = pair.getValue();
break;
}
}
break;
case "bind":
Element bindEl = (Element) child;
host = bindEl.getAttribute("host");
port = Integer.parseInt(bindEl.getAttribute("port"));
break;
case "cache":
Element cacheEl = (Element) child;
cacheEl.getAttributeNode("xs:type").getSchemaTypeInfo();
if ("gbcs:fileSystemCacheType".equals(cacheEl.getAttribute("xs:type"))) {
String cacheFolder = cacheEl.getAttribute("path");
Path cachePath = !cacheFolder.isEmpty()
? Paths.get(cacheFolder)
: Paths.get(System.getProperty("user.home")).resolve(".gbcs");
String maxAgeStr = cacheEl.getAttribute("max-age");
Duration maxAge = !maxAgeStr.isEmpty()
? Duration.parse(maxAgeStr)
: Duration.ofDays(1);
// cache = new Configuration.FileSystemCache(cachePath, maxAge);
}
break;
case "authentication":
for (Node gchild : iterableOf((Element) child)) {
switch (gchild.getNodeName()) {
case "basic":
authentication = new Configuration.BasicAuthentication();
break;
case "client-certificate":
Configuration.TlsCertificateExtractor tlsExtractorUser = null;
Configuration.TlsCertificateExtractor tlsExtractorGroup = null;
for (Node authChild : iterableOf((Element) gchild)) {
Element authEl = (Element) authChild;
switch (authChild.getNodeName()) {
case "group-extractor":
String groupAttrName = authEl.getAttribute("attribute-name");
String groupPattern = authEl.getAttribute("pattern");
tlsExtractorGroup = new Configuration.TlsCertificateExtractor(groupAttrName, groupPattern);
break;
case "user-extractor":
String userAttrName = authEl.getAttribute("attribute-name");
String userPattern = authEl.getAttribute("pattern");
tlsExtractorUser = new Configuration.TlsCertificateExtractor(userAttrName, userPattern);
break;
}
}
authentication = new Configuration.ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup);
break;
}
}
break;
case "tls":
Element tlsEl = (Element) child;
boolean verifyClients = !tlsEl.getAttribute("verify-clients").isEmpty() &&
Boolean.parseBoolean(tlsEl.getAttribute("verify-clients"));
Configuration.KeyStore keyStore = null;
Configuration.TrustStore trustStore = null;
for (Node gchild : iterableOf(tlsEl)) {
Element tlsChild = (Element) gchild;
switch (gchild.getNodeName()) {
case "keystore":
Path keyStoreFile = Paths.get(tlsChild.getAttribute("file"));
String keyStorePassword = !tlsChild.getAttribute("password").isEmpty()
? tlsChild.getAttribute("password")
: null;
String keyAlias = tlsChild.getAttribute("key-alias");
String keyPassword = !tlsChild.getAttribute("key-password").isEmpty()
? tlsChild.getAttribute("key-password")
: null;
keyStore = new Configuration.KeyStore(keyStoreFile, keyStorePassword, keyAlias, keyPassword);
break;
case "truststore":
Path trustStoreFile = Paths.get(tlsChild.getAttribute("file"));
String trustStorePassword = !tlsChild.getAttribute("password").isEmpty()
? tlsChild.getAttribute("password")
: null;
boolean checkCertificateStatus = !tlsChild.getAttribute("check-certificate-status").isEmpty() &&
Boolean.parseBoolean(tlsChild.getAttribute("check-certificate-status"));
trustStore = new Configuration.TrustStore(trustStoreFile, trustStorePassword, checkCertificateStatus);
break;
}
}
tls = new Configuration.Tls(keyStore, trustStore, verifyClients);
break;
}
}
return Configuration.of(host, port, serverPath, users, groups, cache, authentication, tls, useVirtualThread);
}
private static Set<Role> parseRoles(Element root) {
return StreamSupport.stream(iterableOf(root).spliterator(), false)
.map(node -> switch (node.getNodeName()) {
case "reader" -> Role.Reader;
case "writer" -> Role.Writer;
default -> throw new UnsupportedOperationException("Illegal node '" + node.getNodeName() + "'");
})
.collect(Collectors.toSet());
}
private static Set<String> parseUserRefs(Element root) {
return StreamSupport.stream(iterableOf(root).spliterator(), false)
.filter(node -> "user".equals(node.getNodeName()))
.map(node -> ((Element) node).getAttribute("ref"))
.collect(Collectors.toSet());
}
private static Map<String, Configuration.User> parseUsers(Element root) {
return StreamSupport.stream(iterableOf(root).spliterator(), false)
.filter(node -> "user".equals(node.getNodeName()))
.map(node -> {
Element el = (Element) node;
String username = el.getAttribute("name");
String password = !el.getAttribute("password").isEmpty() ? el.getAttribute("password") : null;
return new AbstractMap.SimpleEntry<>(username, new Configuration.User(username, password, Collections.emptySet()));
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static Map.Entry<Map<String, Configuration.User>, Map<String, Configuration.Group>> parseGroups(Element root, Map<String, Configuration.User> knownUsers) {
Map<String, Set<String>> userGroups = new HashMap<>();
Map<String, Configuration.Group> groups = StreamSupport.stream(iterableOf(root).spliterator(), false)
.filter(node -> "group".equals(node.getNodeName()))
.map(node -> {
Element el = (Element) node;
String groupName = el.getAttribute("name");
Set<Role> roles = Collections.emptySet();
for (Node child : iterableOf(el)) {
switch (child.getNodeName()) {
case "users":
parseUserRefs((Element) child).stream()
.map(knownUsers::get)
.filter(Objects::nonNull)
.forEach(user ->
userGroups.computeIfAbsent(user.getName(), k -> new HashSet<>())
.add(groupName));
break;
case "roles":
roles = parseRoles((Element) child);
break;
}
}
return new AbstractMap.SimpleEntry<>(groupName, new Configuration.Group(groupName, roles));
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Map<String, Configuration.User> users = knownUsers.entrySet().stream()
.map(entry -> {
String name = entry.getKey();
Configuration.User user = entry.getValue();
Set<Configuration.Group> userGroupSet = userGroups.getOrDefault(name, Collections.emptySet()).stream()
.map(groups::get)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return new AbstractMap.SimpleEntry<>(name, new Configuration.User(name, user.getPassword(), userGroupSet));
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return new AbstractMap.SimpleEntry<>(users, groups);
}
private static Iterable<Node> iterableOf(Element element) {
return () -> new Iterator<Node>() {
private Node current = element.getFirstChild();
@Override
public boolean hasNext() {
while (current != null && !(current instanceof Element)) {
current = current.getNextSibling();
}
return current != null;
}
@Override
public Node next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Node result = current;
current = current.getNextSibling();
return result;
}
};
}
}

View File

@@ -0,0 +1,5 @@
package net.woggioni.gbcs.api;
public enum Role {
Reader, Writer
}

View File

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

View File

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

21
gbcs-base/build.gradle Normal file
View File

@@ -0,0 +1,21 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'java-library'
alias catalog.plugins.kotlin.jvm
}
dependencies {
compileOnly project(':gbcs-api')
compileOnly catalog.slf4j.api
}
tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs.base=' + project.sourceSets.main.output.asPath
options.javaModuleVersion = version
}
tasks.named("compileKotlin", KotlinCompile.class) {
compilerOptions.jvmTarget = JvmTarget.JVM_21
}

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package net.woggioni.gbcs.base
data class HostAndPort(val host: String, val port: Int = 0) {
override fun toString(): String {
return "$host:$port"
}
}

View File

@@ -0,0 +1,104 @@
package net.woggioni.gbcs.base
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isTraceEnabled) {
val (format, params) = messageBuilder()
trace(format, params)
}
}
inline fun Logger.debugParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isDebugEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.infoParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isInfoEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.warnParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isWarnEnabled) {
val (format, params) = messageBuilder()
warn(format, params)
}
}
inline fun Logger.errorParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isErrorEnabled) {
val (format, params) = messageBuilder()
error(format, params)
}
}
inline fun log(log : Logger,
filter : Logger.() -> Boolean,
loggerMethod : Logger.(String) -> Unit, messageBuilder : () -> String) {
if(log.filter()) {
log.loggerMethod(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder : () -> String) {
if(isTraceEnabled) {
trace(messageBuilder())
}
}
inline fun Logger.debug(messageBuilder : () -> String) {
if(isDebugEnabled) {
debug(messageBuilder())
}
}
inline fun Logger.info(messageBuilder : () -> String) {
if(isInfoEnabled) {
info(messageBuilder())
}
}
inline fun Logger.warn(messageBuilder : () -> String) {
if(isWarnEnabled) {
warn(messageBuilder())
}
}
inline fun Logger.error(messageBuilder : () -> String) {
if(isErrorEnabled) {
error(messageBuilder())
}
}
class LoggingConfig {
init {
val logManager = LogManager.getLogManager()
System.getProperty("log.config.source")?.let withSource@ { source ->
val urls = LoggingConfig::class.java.classLoader.getResources(source)
while(urls.hasMoreElements()) {
val url = urls.nextElement()
url.openStream().use { inputStream ->
logManager.readConfiguration(inputStream)
return@withSource
}
}
Path.of(source).takeIf(Files::exists)
?.let(Files::newInputStream)
?.use(logManager::readConfiguration)
}
}
}

View File

@@ -1,4 +1,4 @@
package net.woggioni.gbcs
package net.woggioni.gbcs.base
import org.slf4j.LoggerFactory
import org.w3c.dom.Document
@@ -72,7 +72,7 @@ class ElementIterator(parent: Element, name: String? = null) : Iterator<Element>
}
}
class Xml(private val doc: Document, val element: Element) {
class Xml(val doc: Document, val element: Element) {
class ErrorHandler(private val fileURL: URL) : ErrHandler {
@@ -127,7 +127,7 @@ class Xml(private val doc: Document, val element: Element) {
fun getSchema(schema: URL): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, true)
sf.setFeature(FEATURE_SECURE_PROCESSING, false)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD)
sf.errorHandler = ErrorHandler(schema)
@@ -144,13 +144,15 @@ class Xml(private val doc: Document, val element: Element) {
fun newDocumentBuilderFactory(schemaResourceURL: URL?): DocumentBuilderFactory {
val dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature(FEATURE_SECURE_PROCESSING, true)
disableProperty(dbf, ACCESS_EXTERNAL_SCHEMA)
dbf.setFeature(FEATURE_SECURE_PROCESSING, false)
// disableProperty(dbf, ACCESS_EXTERNAL_SCHEMA)
dbf.setAttribute(ACCESS_EXTERNAL_SCHEMA, "all")
disableProperty(dbf, ACCESS_EXTERNAL_DTD)
dbf.isExpandEntityReferences = false
dbf.isExpandEntityReferences = true
dbf.isIgnoringComments = true
dbf.isNamespaceAware = true
dbf.isValidating = false
dbf.setFeature("http://apache.org/xml/features/validation/schema", true);
schemaResourceURL?.let {
dbf.schema = getSchema(it)
}
@@ -233,10 +235,11 @@ class Xml(private val doc: Document, val element: Element) {
fun node(
name: String,
namespaceURI : String? = null,
attrs: Map<String, String> = emptyMap(),
cb: Xml.(el: Element) -> Unit = {}
): Element {
val child = doc.createElement(name)
val child = doc.createElementNS(namespaceURI, name)
for ((key, value) in attrs) {
child.setAttribute(key, value)
}
@@ -248,12 +251,16 @@ class Xml(private val doc: Document, val element: Element) {
}
fun attrs(vararg attributes: Pair<String, String>) {
for (attr in attributes) element.setAttribute(attr.first, attr.second)
}
// fun attrs(vararg attributes: Pair<String, String>) {
// for (attr in attributes) element.setAttribute(attr.first, attr.second)
// }
//
// fun attrs(vararg attributes: Pair<Pair<String?, String>, String>) {
// for (attr in attributes) element.setAttributeNS(attr.first.first, attr.first.second, attr.second)
// }
fun attr(key: String, value: String) {
element.setAttribute(key, value)
fun attr(key: String, value: String, namespaceURI : String? = null) {
element.setAttributeNS(namespaceURI, key, value)
}
fun text(txt: String) {

View File

@@ -0,0 +1,64 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id 'java-library'
id 'maven-publish'
alias catalog.plugins.kotlin.jvm
}
configurations {
bundle {
extendsFrom runtimeClasspath
canBeResolved = true
canBeConsumed = false
visible = false
resolutionStrategy {
dependencies {
exclude group: 'org.slf4j', module: 'slf4j-api'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
exclude group: 'org.jetbrains', module: 'annotations'
}
}
}
}
dependencies {
compileOnly project(':gbcs-base')
compileOnly project(':gbcs-api')
compileOnly catalog.jwo
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) {
from(tasks.named(JavaPlugin.JAR_TASK_NAME))
from(configurations.bundle)
group = BasePlugin.BUILD_GROUP
}
tasks.named(BasePlugin.ASSEMBLE_TASK_NAME) {
dependsOn(bundleTask)
}
def bundleArtifact = artifacts.add('archives', bundleTask.get().archiveFile.get().asFile) {
type = 'tar'
builtBy bundleTask
}
publishing {
publications {
maven(MavenPublication) {
artifact bundleArtifact
}
}
}

View File

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

View File

@@ -0,0 +1,60 @@
package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.MemcachedClient
import net.rubyeye.xmemcached.XMemcachedClientBuilder
import net.rubyeye.xmemcached.command.BinaryCommandFactory
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.rubyeye.xmemcached.transcoders.SerializingTranscoder
import net.woggioni.gbcs.api.Cache
import net.woggioni.gbcs.api.exception.ContentTooLargeException
import net.woggioni.gbcs.base.HostAndPort
import net.woggioni.jwo.JWO
import java.io.ByteArrayInputStream
import java.net.InetSocketAddress
import java.nio.channels.Channels
import java.nio.channels.ReadableByteChannel
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.Duration
class MemcachedCache(
servers: List<HostAndPort>,
private val maxAge: Duration,
maxSize : Int,
digestAlgorithm: String?,
compressionMode: CompressionMode,
) : Cache {
private val memcachedClient = XMemcachedClientBuilder(
servers.stream().map { addr: HostAndPort -> InetSocketAddress(addr.host, addr.port) }.toList()
).apply {
commandFactory = BinaryCommandFactory()
digestAlgorithm?.let { dAlg ->
setKeyProvider { key ->
val md = MessageDigest.getInstance(dAlg)
md.update(key.toByteArray(StandardCharsets.UTF_8))
JWO.bytesToHex(md.digest())
}
}
transcoder = SerializingTranscoder(maxSize).apply {
setCompressionMode(compressionMode)
}
}.build()
override fun get(key: String): ReadableByteChannel? {
return memcachedClient.get<ByteArray>(key)
?.let(::ByteArrayInputStream)
?.let(Channels::newChannel)
}
override fun put(key: String, content: ByteArray) {
try {
memcachedClient[key, maxAge.toSeconds().toInt()] = content
} catch (e: IllegalArgumentException) {
throw ContentTooLargeException(e.message, e)
}
}
override fun close() {
memcachedClient.shutdown()
}
}

View File

@@ -0,0 +1,26 @@
package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.HostAndPort
import java.time.Duration
data class MemcachedCacheConfiguration(
var servers: List<HostAndPort>,
var maxAge: Duration = Duration.ofDays(1),
var maxSize: Int = 0x100000,
var digestAlgorithm: String? = null,
var compressionMode: CompressionMode = CompressionMode.ZIP,
) : Configuration.Cache {
override fun materialize() = MemcachedCache(
servers,
maxAge,
maxSize,
digestAlgorithm,
compressionMode
)
override fun getNamespaceURI() = "urn:net.woggioni.gbcs-memcached"
override fun getTypeName() = "memcachedCacheType"
}

View File

@@ -0,0 +1,85 @@
package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.HostAndPort
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.time.Duration
import java.util.zip.Deflater
class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/memcached/schema/gbcs-memcached.xsd"
override fun getXmlType() = "memcachedCacheType"
override fun getXmlNamespace()= "urn:net.woggioni.gbcs-memcached"
override fun deserialize(el: Element): MemcachedCacheConfiguration {
val servers = mutableListOf<HostAndPort>()
val maxAge = el.getAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse)
?: Duration.ofDays(1)
val maxSize = el.getAttribute("max-size")
.takeIf(String::isNotEmpty)
?.let(String::toInt)
?: 0x100000
val enableCompression = el.getAttribute("enable-compression")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: false
val compressionMode = el.getAttribute("compression-mode")
.takeIf(String::isNotEmpty)
?.let {
when(it) {
"gzip" -> CompressionMode.GZIP
"zip" -> CompressionMode.ZIP
else -> CompressionMode.ZIP
}
}
?: CompressionMode.ZIP
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty)
for (child in el.asIterable()) {
when (child.nodeName) {
"server" -> {
servers.add(HostAndPort(child.getAttribute("host"), child.getAttribute("port").toInt()))
}
}
}
return MemcachedCacheConfiguration(
servers,
maxAge,
maxSize,
digestAlgorithm,
compressionMode,
)
}
override fun serialize(doc: Document, cache : MemcachedCacheConfiguration) = cache.run {
val result = doc.createElementNS(xmlNamespace,"cache")
Xml.of(doc, result) {
attr("xs:type", xmlType, GBCS.XML_SCHEMA_NAMESPACE_URI)
for (server in servers) {
node("server", xmlNamespace) {
attr("host", server.host)
attr("port", server.port.toString())
}
}
attr("max-age", maxAge.toString())
attr("max-size", maxSize.toString())
digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm)
}
attr("compression-mode", when(compressionMode) {
CompressionMode.GZIP -> "gzip"
CompressionMode.ZIP -> "zip"
})
}
result
}
}

View File

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

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs-memcached"
xmlns:gbcs-memcached="urn:net.woggioni.gbcs-memcached"
xmlns:gbcs="urn:net.woggioni.gbcs"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="classpath:net/woggioni/gbcs/schema/gbcs.xsd" namespace="urn:net.woggioni.gbcs"/>
<xs:complexType name="memcachedServerType">
<xs:attribute name="host" type="xs:string" use="required"/>
<xs:attribute name="port" type="xs:positiveInteger" use="required"/>
</xs:complexType>
<xs:complexType name="memcachedCacheType">
<xs:complexContent>
<xs:extension base="gbcs:cacheType">
<xs:sequence maxOccurs="unbounded">
<xs:element name="server" type="gbcs-memcached:memcachedServerType"/>
</xs:sequence>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="max-size" type="xs:unsignedInt" default="1048576"/>
<xs:attribute name="digest" type="xs:token" />
<xs:attribute name="compression-type" type="gbcs-memcached:compressionType" default="deflate"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="compressionType">
<xs:restriction base="xs:token">
<xs:enumeration value="deflate"/>
<xs:enumeration value="gzip"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@@ -1,5 +1,9 @@
gbcs.version = 1.0.0
org.gradle.configuration-cache=false
org.gradle.parallel=true
org.gradle.caching=true
lys.version = 2024.12.21
gbcs.version = 0.0.1
lys.version = 2025.01.08
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

7
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

2
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################

View File

@@ -1 +1,2 @@
Args=-H:Optimize=3 --gc=serial --libc=musl --static -H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
Args=-H:Optimize=3 --gc=serial
#-H:TraceClassInitialization=io.netty.handler.ssl.BouncyCastleAlpnSslUtils

View File

@@ -20,10 +20,14 @@ dependencyResolutionManagement {
versionCatalogs {
catalog {
from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version')
version('envelope', '2024.12.15')
}
}
}
rootProject.name = 'gbcs'
include 'gbcs-api'
include 'gbcs-base'
include 'gbcs-memcached'

View File

@@ -0,0 +1,15 @@
open module net.woggioni.gbcs.test {
requires net.woggioni.gbcs;
requires net.woggioni.gbcs.api;
requires java.naming;
requires org.bouncycastle.pkix;
requires org.bouncycastle.provider;
requires io.netty.codec.http;
requires net.woggioni.gbcs.base;
requires java.net.http;
requires static lombok;
requires org.junit.jupiter.params;
exports net.woggioni.gbcs.test to org.junit.platform.commons;
// opens net.woggioni.gbcs.test to org.junit.platform.commons;
}

View File

@@ -0,0 +1,51 @@
package net.woggioni.gbcs.test;
import net.woggioni.gbcs.GradleBuildCacheServer;
import net.woggioni.gbcs.api.Configuration;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public abstract class AbstractServerTest {
protected Configuration cfg;
protected Path testDir;
private GradleBuildCacheServer.ServerHandle serverHandle;
@BeforeAll
public void setUp0(@TempDir Path tmpDir) {
this.testDir = tmpDir;
setUp();
startServer(cfg);
}
@AfterAll
public void tearDown0() {
tearDown();
stopServer();
}
protected abstract void setUp();
protected abstract void tearDown();
private void startServer(Configuration cfg) {
this.serverHandle = new GradleBuildCacheServer(cfg).run();
}
private void stopServer() {
if (serverHandle != null) {
try (GradleBuildCacheServer.ServerHandle handle = serverHandle) {
handle.shutdown();
}
}
}
}

View File

@@ -0,0 +1,229 @@
package net.woggioni.gbcs.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import lombok.SneakyThrows;
import net.woggioni.gbcs.AbstractNettyHttpAuthenticator;
import net.woggioni.gbcs.api.Role;
import net.woggioni.gbcs.base.Xml;
import net.woggioni.gbcs.api.Configuration;
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration;
import net.woggioni.gbcs.configuration.Serializer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Deflater;
import java.io.IOException;
public class BasicAuthServerTest extends AbstractServerTest {
private static final String PASSWORD = "password";
private Path cacheDir;
private final Random random = new Random(101325);
private final Map.Entry<String, byte[]> keyValuePair;
public BasicAuthServerTest() {
this.keyValuePair = newEntry(random);
}
@Override
@SneakyThrows
protected void setUp() {
this.cacheDir = testDir.resolve("cache");
Configuration.Group readersGroup = new Configuration.Group("readers", Set.of(Role.Reader));
Configuration.Group writersGroup = new Configuration.Group("writers", Set.of(Role.Writer));
List<Configuration.User> users = Arrays.asList(
new Configuration.User("user1", AbstractNettyHttpAuthenticator.Companion.hashPassword(PASSWORD, null), Set.of(readersGroup)),
new Configuration.User("user2", AbstractNettyHttpAuthenticator.Companion.hashPassword(PASSWORD, null), Set.of(writersGroup)),
new Configuration.User("user3", AbstractNettyHttpAuthenticator.Companion.hashPassword(PASSWORD, null), Set.of(readersGroup, writersGroup))
);
Map<String, Configuration.User> usersMap = users.stream()
.collect(Collectors.toMap(user -> user.getName(), user -> user));
Map<String, Configuration.Group> groupsMap = Stream.of(writersGroup, readersGroup)
.collect(Collectors.toMap(group -> group.getName(), group -> group));
cfg = new Configuration(
"127.0.0.1",
new ServerSocket(0).getLocalPort() + 1,
"/",
usersMap,
groupsMap,
new FileSystemCacheConfiguration(
this.cacheDir,
Duration.ofSeconds(3600 * 24),
"MD5",
false,
Deflater.DEFAULT_COMPRESSION
),
new Configuration.BasicAuthentication(),
null,
true
);
Xml.Companion.write(Serializer.INSTANCE.serialize(cfg), System.out);
}
@Override
protected void tearDown() {
// Empty implementation
}
private String buildAuthorizationHeader(Configuration.User user, String password) {
String credentials = user.getName() + ":" + password;
byte[] encodedCredentials = Base64.getEncoder().encode(credentials.getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(encodedCredentials, StandardCharsets.UTF_8);
}
private HttpRequest.Builder newRequestBuilder(String key) {
return HttpRequest.newBuilder()
.uri(URI.create(String.format("http://%s:%d/%s", cfg.getHost(), cfg.getPort(), key)));
}
private Map.Entry<String, byte[]> newEntry(Random random) {
byte[] keyBytes = new byte[0x10];
random.nextBytes(keyBytes);
String key = Base64.getUrlEncoder().encodeToString(keyBytes);
byte[] value = new byte[0x1000];
random.nextBytes(value);
return Map.entry(key, value);
}
@Test
@Order(1)
public void putWithNoAuthorizationHeader() throws IOException, InterruptedException {
try(HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode());
}
}
@Test
@Order(2)
public void putAsAReaderUser() throws IOException, InterruptedException {
try(HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Reader) && !u.getRoles().contains(Role.Writer))
.findFirst()
.orElseThrow(() -> new RuntimeException("Reader user not found"));
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode());
}
}
@Test
@Order(3)
public void getAsAWriterUser() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Writer))
.findFirst()
.orElseThrow(() -> new RuntimeException("Writer user not found"));
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET();
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode());
}
}
@Test
@Order(4)
public void putAsAWriterUser() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Writer))
.findFirst()
.orElseThrow(() -> new RuntimeException("Writer user not found"));
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode());
}
}
@Test
@Order(5)
public void getAsAReaderUser() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Reader))
.findFirst()
.orElseThrow(() -> new RuntimeException("Reader user not found"));
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET();
HttpResponse<byte[]> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode());
Assertions.assertArrayEquals(value, response.body());
}
}
@Test
@Order(6)
public void getMissingKeyAsAReaderUser() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
Map.Entry<String, byte[]> entry = newEntry(random);
String key = entry.getKey();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Reader))
.findFirst()
.orElseThrow(() -> new RuntimeException("Reader user not found"));
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET();
HttpResponse<byte[]> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode());
}
}
}

View File

@@ -0,0 +1,48 @@
package net.woggioni.gbcs.test;
import net.woggioni.gbcs.GradleBuildCacheServer;
import net.woggioni.gbcs.base.GBCS;
import net.woggioni.gbcs.base.Xml;
import net.woggioni.gbcs.configuration.Parser;
import net.woggioni.gbcs.configuration.Serializer;
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
class ConfigurationTest {
@ParameterizedTest
@ValueSource(strings = {
// "classpath:net/woggioni/gbcs/gbcs-default.xml",
"classpath:net/woggioni/gbcs/test/gbcs-memcached.xml"
})
void test(String configurationUrl, @TempDir Path testDir) throws IOException {
URL.setURLStreamHandlerFactory(new ClasspathUrlStreamHandlerFactoryProvider());
// DocumentBuilderFactory dbf = Xml.newDocumentBuilderFactory(GradleBuildCacheServer.CONFIGURATION_SCHEMA_URL);
// DocumentBuilder db = dbf.newDocumentBuilder();
// URL configurationUrl = GradleBuildCacheServer.DEFAULT_CONFIGURATION_URL;
var doc = Xml.Companion.parseXml(GBCS.INSTANCE.toUrl(configurationUrl), null, null);
var cfg = Parser.INSTANCE.parse(doc);
Path configFile = testDir.resolve("gbcs.xml");
try (var outputStream = Files.newOutputStream(configFile)) {
Xml.Companion.write(Serializer.INSTANCE.serialize(cfg), outputStream);
}
Xml.Companion.write(Serializer.INSTANCE.serialize(cfg), System.out);
var parsed = Parser.INSTANCE.parse(Xml.Companion.parseXml(
configFile.toUri().toURL(), null, null
));
Assertions.assertEquals(cfg, parsed);
}
}

View File

@@ -0,0 +1,128 @@
package net.woggioni.gbcs.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import lombok.SneakyThrows;
import net.woggioni.gbcs.base.Xml;
import net.woggioni.gbcs.api.Configuration;
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration;
import net.woggioni.gbcs.configuration.Serializer;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
import java.util.zip.Deflater;
import java.io.IOException;
public class NoAuthServerTest extends AbstractServerTest {
private Path cacheDir;
private final Random random = new Random(101325);
private final Map.Entry<String, byte[]> keyValuePair;
public NoAuthServerTest() {
this.keyValuePair = newEntry(random);
}
@Override
@SneakyThrows
protected void setUp() {
this.cacheDir = testDir.resolve("cache");
cfg = new Configuration(
"127.0.0.1",
new ServerSocket(0).getLocalPort() + 1,
"/",
Collections.emptyMap(),
Collections.emptyMap(),
new FileSystemCacheConfiguration(
this.cacheDir,
Duration.ofSeconds(3600 * 24),
"MD5",
true,
Deflater.DEFAULT_COMPRESSION
),
null,
null,
true
);
Xml.Companion.write(Serializer.INSTANCE.serialize(cfg), System.out);
}
@Override
protected void tearDown() {
// Empty implementation
}
private HttpRequest.Builder newRequestBuilder(String key) {
return HttpRequest.newBuilder()
.uri(URI.create(String.format("http://%s:%d/%s", cfg.getHost(), cfg.getPort(), key)));
}
private Map.Entry<String, byte[]> newEntry(Random random) {
byte[] keyBytes = new byte[0x10];
random.nextBytes(keyBytes);
String key = Base64.getUrlEncoder().encodeToString(keyBytes);
byte[] value = new byte[0x1000];
random.nextBytes(value);
return Map.entry(key, value);
}
@Test
@Order(1)
public void putWithNoAuthorizationHeader() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode());
}
}
@Test
@Order(2)
public void getWithNoAuthorizationHeader() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.GET();
HttpResponse<byte[]> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode());
Assertions.assertArrayEquals(value, response.body());
}
}
@Test
@Order(3)
public void getMissingKey() throws IOException, InterruptedException {
try (HttpClient client = HttpClient.newHttpClient()) {
Map.Entry<String, byte[]> entry = newEntry(random);
String key = entry.getKey();
HttpRequest.Builder requestBuilder = newRequestBuilder(key).GET();
HttpResponse<byte[]> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode());
}
}
}

View File

@@ -0,0 +1,357 @@
package net.woggioni.gbcs.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import lombok.SneakyThrows;
import net.woggioni.gbcs.api.Configuration;
import net.woggioni.gbcs.api.Role;
import net.woggioni.gbcs.base.Xml;
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration;
import net.woggioni.gbcs.configuration.Serializer;
import net.woggioni.gbcs.utils.CertificateUtils;
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials;
import org.bouncycastle.asn1.x500.X500Name;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStore.PasswordProtection;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.Deflater;
public class TlsServerTest extends AbstractServerTest {
private static final String CA_CERTIFICATE_ENTRY = "gbcs-ca";
private static final String CLIENT_CERTIFICATE_ENTRY = "gbcs-client";
private static final String SERVER_CERTIFICATE_ENTRY = "gbcs-server";
private static final String PASSWORD = "password";
private Path cacheDir;
private Path serverKeyStoreFile;
private Path clientKeyStoreFile;
private Path trustStoreFile;
private KeyStore serverKeyStore;
private KeyStore clientKeyStore;
private KeyStore trustStore;
private X509Credentials ca;
private final Configuration.Group readersGroup = new Configuration.Group("readers", Set.of(Role.Reader));
private final Configuration.Group writersGroup = new Configuration.Group("writers", Set.of(Role.Writer));
private final Random random = new Random(101325);
private final Map.Entry<String, byte[]> keyValuePair;
private final List<Configuration.User> users = Arrays.asList(
new Configuration.User("user1", null, Set.of(readersGroup)),
new Configuration.User("user2", null, Set.of(writersGroup)),
new Configuration.User("user3", null, Set.of(readersGroup, writersGroup))
);
public TlsServerTest() {
this.keyValuePair = newEntry(random);
}
private void createKeyStoreAndTrustStore() throws Exception {
ca = CertificateUtils.createCertificateAuthority(CA_CERTIFICATE_ENTRY, 30);
var serverCert = CertificateUtils.createServerCertificate(ca, new X500Name("CN=" + SERVER_CERTIFICATE_ENTRY), 30);
var clientCert = CertificateUtils.createClientCertificate(ca, new X500Name("CN=" + CLIENT_CERTIFICATE_ENTRY), 30);
serverKeyStore = KeyStore.getInstance("PKCS12");
serverKeyStore.load(null, null);
serverKeyStore.setEntry(
CA_CERTIFICATE_ENTRY,
new KeyStore.TrustedCertificateEntry(ca.certificate()),
new PasswordProtection(null)
);
serverKeyStore.setEntry(
SERVER_CERTIFICATE_ENTRY,
new KeyStore.PrivateKeyEntry(
serverCert.keyPair().getPrivate(),
new java.security.cert.Certificate[]{serverCert.certificate(), ca.certificate()}
),
new PasswordProtection(PASSWORD.toCharArray())
);
try (var out = Files.newOutputStream(this.serverKeyStoreFile)) {
serverKeyStore.store(out, null);
}
clientKeyStore = KeyStore.getInstance("PKCS12");
clientKeyStore.load(null, null);
clientKeyStore.setEntry(
CA_CERTIFICATE_ENTRY,
new KeyStore.TrustedCertificateEntry(ca.certificate()),
new PasswordProtection(null)
);
clientKeyStore.setEntry(
CLIENT_CERTIFICATE_ENTRY,
new KeyStore.PrivateKeyEntry(
clientCert.keyPair().getPrivate(),
new java.security.cert.Certificate[]{clientCert.certificate(), ca.certificate()}
),
new PasswordProtection(PASSWORD.toCharArray())
);
try (var out = Files.newOutputStream(this.clientKeyStoreFile)) {
clientKeyStore.store(out, null);
}
trustStore = KeyStore.getInstance("PKCS12");
trustStore.load(null, null);
trustStore.setEntry(
CA_CERTIFICATE_ENTRY,
new KeyStore.TrustedCertificateEntry(ca.certificate()),
new PasswordProtection(null)
);
try (var out = Files.newOutputStream(this.trustStoreFile)) {
trustStore.store(out, null);
}
}
private KeyStore getClientKeyStore(X509Credentials ca, X500Name subject) throws Exception {
var clientCert = CertificateUtils.createClientCertificate(ca, subject, 30);
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null, null);
keyStore.setEntry(
CA_CERTIFICATE_ENTRY,
new KeyStore.TrustedCertificateEntry(ca.certificate()),
new PasswordProtection(null)
);
keyStore.setEntry(
CLIENT_CERTIFICATE_ENTRY,
new KeyStore.PrivateKeyEntry(
clientCert.keyPair().getPrivate(),
new java.security.cert.Certificate[]{clientCert.certificate(), ca.certificate()}
),
new PasswordProtection(PASSWORD.toCharArray())
);
return keyStore;
}
private HttpClient getHttpClient(KeyStore clientKeyStore) throws Exception {
KeyManagerFactory kmf = null;
if (clientKeyStore != null) {
kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientKeyStore, PASSWORD.toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(
kmf != null ? kmf.getKeyManagers() : null,
tmf.getTrustManagers(),
null
);
return HttpClient.newBuilder().sslContext(sslContext).build();
}
@Override
@SneakyThrows
protected void setUp() {
this.clientKeyStoreFile = testDir.resolve("client-keystore.p12");
this.serverKeyStoreFile = testDir.resolve("server-keystore.p12");
this.trustStoreFile = testDir.resolve("truststore.p12");
this.cacheDir = testDir.resolve("cache");
createKeyStoreAndTrustStore();
Map<String, Configuration.User> usersMap = users.stream()
.collect(Collectors.toMap(user -> user.getName(), user -> user));
Map<String, Configuration.Group> groupsMap = Stream.of(writersGroup, readersGroup)
.collect(Collectors.toMap(group -> group.getName(), group -> group));
cfg = new Configuration(
"127.0.0.1",
new ServerSocket(0).getLocalPort() + 1,
"gbcs",
usersMap,
groupsMap,
new FileSystemCacheConfiguration(
this.cacheDir,
Duration.ofSeconds(3600 * 24),
"MD5",
true,
Deflater.DEFAULT_COMPRESSION
),
new Configuration.ClientCertificateAuthentication(
new Configuration.TlsCertificateExtractor("CN", "(.*)"),
null
),
new Configuration.Tls(
new Configuration.KeyStore(this.serverKeyStoreFile, null, SERVER_CERTIFICATE_ENTRY, PASSWORD),
new Configuration.TrustStore(this.trustStoreFile, null, false),
true
),
true
);
Xml.Companion.write(Serializer.INSTANCE.serialize(cfg), System.out);
}
@Override
protected void tearDown() {
// Empty implementation
}
private HttpRequest.Builder newRequestBuilder(String key) {
return HttpRequest.newBuilder()
.uri(URI.create(String.format("https://%s:%d/%s", cfg.getHost(), cfg.getPort(), key)));
}
private String buildAuthorizationHeader(Configuration.User user, String password) {
String credentials = user.getName() + ":" + password;
byte[] encodedCredentials = Base64.getEncoder().encode(credentials.getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(encodedCredentials, StandardCharsets.UTF_8);
}
private Map.Entry<String, byte[]> newEntry(Random random) {
byte[] keyBytes = new byte[0x10];
random.nextBytes(keyBytes);
String key = Base64.getUrlEncoder().encodeToString(keyBytes);
byte[] value = new byte[0x1000];
random.nextBytes(value);
return Map.entry(key, value);
}
@Test
@Order(1)
public void putWithNoClientCertificate() throws Exception {
try (HttpClient client = getHttpClient(null)) {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode());
}
}
@Test
@Order(2)
public void putAsAReaderUser() throws Exception {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Reader) && !u.getRoles().contains(Role.Writer))
.findFirst()
.orElseThrow(() -> new RuntimeException("Reader user not found"));
try (HttpClient client = getHttpClient(getClientKeyStore(ca, new X500Name("CN=" + user.getName())))) {
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode());
}
}
@Test
@Order(3)
public void getAsAWriterUser() throws Exception {
String key = keyValuePair.getKey();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Writer))
.findFirst()
.orElseThrow(() -> new RuntimeException("Writer user not found"));
try (HttpClient client = getHttpClient(getClientKeyStore(ca, new X500Name("CN=" + user.getName())))) {
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET();
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode());
}
}
@Test
@Order(4)
public void putAsAWriterUser() throws Exception {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Writer))
.findFirst()
.orElseThrow(() -> new RuntimeException("Writer user not found"));
try (HttpClient client = getHttpClient(getClientKeyStore(ca, new X500Name("CN=" + user.getName())))) {
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Content-Type", "application/octet-stream")
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.PUT(HttpRequest.BodyPublishers.ofByteArray(value));
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode());
}
}
@Test
@Order(5)
public void getAsAReaderUser() throws Exception {
String key = keyValuePair.getKey();
byte[] value = keyValuePair.getValue();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Reader))
.findFirst()
.orElseThrow(() -> new RuntimeException("Reader user not found"));
try (HttpClient client = getHttpClient(getClientKeyStore(ca, new X500Name("CN=" + user.getName())))) {
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET();
HttpResponse<byte[]> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode());
Assertions.assertArrayEquals(value, response.body());
}
}
@Test
@Order(6)
public void getMissingKeyAsAReaderUser() throws Exception {
Map.Entry<String, byte[]> entry = newEntry(random);
String key = entry.getKey();
Configuration.User user = cfg.getUsers().values().stream()
.filter(u -> u.getRoles().contains(Role.Reader))
.findFirst()
.orElseThrow(() -> new RuntimeException("Reader user not found"));
try (HttpClient client = getHttpClient(getClientKeyStore(ca, new X500Name("CN=" + user.getName())))) {
HttpRequest.Builder requestBuilder = newRequestBuilder(key)
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
.GET();
HttpResponse<byte[]> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode());
}
}
}

View File

@@ -0,0 +1,28 @@
package net.woggioni.gbcs.test;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.util.Objects;
public class X500NameTest {
@Test
@SneakyThrows
void test() {
final var name =
"C=SG, L=Bugis, CN=woggioni@f6aa5663ef26, emailAddress=oggioni.walter@gmail.com, street=1 Fraser Street\\, Duo Residences #23-05, postalCode=189350, GN=Walter, SN=Oggioni, pseudonym=woggioni";
final var ldapName = new LdapName(name);
final var value = ldapName.getRdns()
.stream()
.filter(it -> Objects.equals("CN", it.getType()))
.findFirst()
.map(Rdn::getValue)
.orElseThrow(Assertions::fail);
Assertions.assertEquals("woggioni@f6aa5663ef26", value);
}
}

View File

@@ -0,0 +1,227 @@
package net.woggioni.gbcs.utils;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectAltPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
public class CertificateUtils {
public record X509Credentials(
KeyPair keyPair,
X509Certificate certificate
){ }
public static class CertificateAuthority {
private final PrivateKey privateKey;
private final X509Certificate certificate;
public CertificateAuthority(PrivateKey privateKey, X509Certificate certificate) {
this.privateKey = privateKey;
this.certificate = certificate;
}
public PrivateKey getPrivateKey() { return privateKey; }
public X509Certificate getCertificate() { return certificate; }
}
/**
* Creates a new Certificate Authority (CA)
* @param commonName The CA's common name
* @param validityDays How long the CA should be valid for
* @return The generated CA containing both private key and certificate
*/
public static X509Credentials createCertificateAuthority(String commonName, int validityDays)
throws Exception {
// Generate key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(4096);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// Prepare certificate data
X500Name issuerName = new X500Name("CN=" + commonName);
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
Instant now = Instant.now();
Date startDate = Date.from(now);
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
// Create certificate builder
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
startDate,
endDate,
issuerName,
keyPair.getPublic()
);
// Add CA extensions
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(true)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
);
// Sign the certificate
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(keyPair.getPrivate());
X509Certificate cert = new JcaX509CertificateConverter()
.getCertificate(certBuilder.build(signer));
return new X509Credentials(keyPair, cert);
}
/**
* Creates a server certificate signed by the CA
* @param ca The Certificate Authority to sign with
* @param subjectName The server's common name
* @param validityDays How long the certificate should be valid for
* @return KeyPair containing the server's private key and certificate
*/
public static X509Credentials createServerCertificate(X509Credentials ca, X500Name subjectName, int validityDays)
throws Exception {
// Generate server key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair serverKeyPair = keyPairGenerator.generateKeyPair();
// Prepare certificate data
X500Name issuerName = new X500Name(ca.certificate().getSubjectX500Principal().getName());
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
Instant now = Instant.now();
Date startDate = Date.from(now);
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
// Create certificate builder
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
startDate,
endDate,
subjectName,
serverKeyPair.getPublic()
);
// Add server certificate extensions
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(false)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)
);
certBuilder.addExtension(
Extension.extendedKeyUsage,
true,
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth})
);
GeneralNames subjectAltNames = GeneralNames.getInstance(
new DERSequence(
new GeneralName[] {
new GeneralName(GeneralName.iPAddress, "127.0.0.1")
}
)
);
certBuilder.addExtension(
Extension.subjectAlternativeName,
true,
subjectAltNames
);
// Sign the certificate
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(ca.keyPair().getPrivate());
X509Certificate cert = new JcaX509CertificateConverter()
.getCertificate(certBuilder.build(signer));
return new X509Credentials(serverKeyPair, cert);
}
/**
* Creates a client certificate signed by the CA
* @param ca The Certificate Authority to sign with
* @param subjectName The client's common name
* @param validityDays How long the certificate should be valid for
* @return KeyPair containing the client's private key and certificate
*/
public static X509Credentials createClientCertificate(X509Credentials ca, X500Name subjectName, int validityDays)
throws Exception {
// Generate client key pair
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair clientKeyPair = keyPairGenerator.generateKeyPair();
// Prepare certificate data
X500Name issuerName = new X500Name(ca.certificate().getSubjectX500Principal().getName());
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
Instant now = Instant.now();
Date startDate = Date.from(now);
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
// Create certificate builder
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
issuerName,
serialNumber,
startDate,
endDate,
subjectName,
clientKeyPair.getPublic()
);
// Add client certificate extensions
certBuilder.addExtension(
Extension.basicConstraints,
true,
new BasicConstraints(false)
);
certBuilder.addExtension(
Extension.keyUsage,
true,
new KeyUsage(KeyUsage.digitalSignature)
);
certBuilder.addExtension(
Extension.extendedKeyUsage,
true,
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth})
);
// Sign the certificate
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.build(ca.keyPair().getPrivate());
X509Certificate cert = new JcaX509CertificateConverter()
.getCertificate(certBuilder.build(signer));
return new X509Credentials(clientKeyPair, cert);
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs"
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">
<bind host="127.0.0.1" port="11443" />
<cache xs:type="gbcs-memcached:memcachedCacheType" max-age="P7D" digest="MD5">
<server host="127.0.0.1" port="11211"/>
</cache>
<authentication>
<none/>
</authentication>
</gbcs:server>

View File

@@ -1,6 +1,14 @@
import net.woggioni.gbcs.api.CacheProvider;
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider;
import net.woggioni.gbcs.cache.FileSystemCacheProvider;
open module net.woggioni.gbcs {
// exports net.woggioni.gbcs.cache to net.woggioni.gbcs.test;
// exports net.woggioni.gbcs.configuration to net.woggioni.gbcs.test;
// exports net.woggioni.gbcs.url to net.woggioni.gbcs.test;
// exports net.woggioni.gbcs to net.woggioni.gbcs.test;
// opens net.woggioni.gbcs.schema to net.woggioni.gbcs.test;
requires java.sql;
requires java.xml;
requires java.logging;
@@ -14,10 +22,16 @@ open module net.woggioni.gbcs {
requires io.netty.codec;
requires org.slf4j;
requires net.woggioni.jwo;
requires net.woggioni.gbcs.base;
requires net.woggioni.gbcs.api;
exports net.woggioni.gbcs;
exports net.woggioni.gbcs.url;
// exports net.woggioni.gbcs;
// exports net.woggioni.gbcs.url;
// opens net.woggioni.gbcs to net.woggioni.envelope;
provides java.net.URLStreamHandlerFactory with ClasspathUrlStreamHandlerFactoryProvider;
uses java.net.URLStreamHandlerFactory;
// uses net.woggioni.gbcs.api.Cache;
uses CacheProvider;
provides CacheProvider with FileSystemCacheProvider;
}

View File

@@ -1,88 +0,0 @@
package net.woggioni.gbcs;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
public class NettyPingServer {
private final int port;
public NettyPingServer(int port) {
this.port = port;
}
public void start() throws Exception {
// Create event loop groups for handling incoming connections and processing
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// Create server bootstrap configuration
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new StringDecoder(CharsetUtil.UTF_8),
new StringEncoder(CharsetUtil.UTF_8),
new PingServerHandler()
);
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// Bind and start the server
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("Ping Server started on port: " + port);
try(final var handle = new GradleBuildCacheServer.ServerHandle(future, bossGroup, workerGroup)) {
Thread.sleep(5000);
future.channel().close();
// Wait until the server socket is closed
future.channel().closeFuture().sync();
}
} finally {
// Shutdown event loop groups
// workerGroup.shutdownGracefully();
// bossGroup.shutdownGracefully();
}
}
// Custom handler for processing ping requests
private static class PingServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
// Check if the received message is a ping request
if ("ping".equalsIgnoreCase(msg.trim())) {
// Respond with "pong"
ctx.writeAndFlush("pong\n");
System.out.println("Received ping, sent pong");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Log and close the connection in case of any errors
cause.printStackTrace();
ctx.close();
}
}
// Main method to start the server
public static void main(String[] args) throws Exception {
int port = 8080; // Default port
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
new NettyPingServer(port).start();
}
}

View File

@@ -1,16 +1,47 @@
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;
@@ -24,10 +55,17 @@ public class ClasspathUrlStreamHandlerFactoryProvider implements URLStreamHandle
@Override
protected URLConnection openConnection(URL u) throws IOException {
final URL resourceUrl = classLoader.getResource(u.getPath());
return Optional.ofNullable(resourceUrl)
.map((Fun<URL, URLConnection>) URL::openConnection)
.orElseThrow(IOException::new);
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);
}
}
@@ -43,4 +81,24 @@ public class ClasspathUrlStreamHandlerFactoryProvider implements URLStreamHandle
}
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

@@ -11,6 +11,7 @@ import io.netty.handler.codec.http.HttpRequest
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

View File

@@ -1,6 +1,7 @@
package net.woggioni.gbcs
import io.netty.handler.codec.http.HttpRequest
import net.woggioni.gbcs.api.Role
fun interface Authorizer {
fun authorize(roles : Set<Role>, request: HttpRequest) : Boolean

View File

@@ -42,22 +42,30 @@ import io.netty.handler.stream.ChunkedNioStream
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.DefaultEventExecutorGroup
import io.netty.util.concurrent.EventExecutorGroup
import net.woggioni.gbcs.cache.Cache
import net.woggioni.gbcs.cache.FileSystemCache
import net.woggioni.gbcs.configuration.Configuration
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.base.GBCS
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.GBCS.toUrl
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.Serializer
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2
import java.io.ByteArrayOutputStream
import java.net.InetSocketAddress
import java.net.URI
import java.net.URL
import java.net.URLStreamHandlerFactory
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.Arrays
@@ -68,6 +76,7 @@ import java.util.regex.Pattern
import javax.naming.ldap.LdapName
import javax.net.ssl.SSLEngine
import javax.net.ssl.SSLPeerUnverifiedException
import kotlin.io.path.absolute
class GradleBuildCacheServer(private val cfg: Configuration) {
@@ -107,7 +116,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
override fun authenticate(ctx: ChannelHandlerContext, req: HttpRequest): Set<Role>? {
return try {
sslEngine.session.peerCertificates
} catch (es : SSLPeerUnverifiedException) {
} catch (es: SSLPeerUnverifiedException) {
null
}?.takeIf {
it.isNotEmpty()
@@ -187,13 +196,13 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
.map { it as X509Certificate }
.toArray { size -> Array<X509Certificate?>(size) { null } }
SslContextBuilder.forServer(serverKey, *serverCert).apply {
if (tls.verifyClients) {
if (tls.isVerifyClients) {
clientAuth(ClientAuth.OPTIONAL)
val trustStore = tls.trustStore
if (trustStore != null) {
val ts = loadKeystore(trustStore.file, trustStore.password)
trustManager(
ClientCertificateValidator.getTrustManager(ts, trustStore.checkCertificateStatus)
ClientCertificateValidator.getTrustManager(ts, trustStore.isCheckCertificateStatus)
)
}
}
@@ -259,7 +268,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
override fun initChannel(ch: Channel) {
val pipeline = ch.pipeline()
val auth = cfg.authentication
var authenticator : AbstractNettyHttpAuthenticator? = null
var authenticator: AbstractNettyHttpAuthenticator? = null
if (auth is Configuration.BasicAuthentication) {
val roleAuthorizer = RoleAuthorizer()
authenticator = (NettyHttpBasicAuthenticator(cfg.users, roleAuthorizer))
@@ -268,7 +277,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val sslHandler = sslContext.newHandler(ch.alloc())
pipeline.addLast(sslHandler)
if(auth is Configuration.ClientCertificateAuthentication) {
if (auth is Configuration.ClientCertificateAuthentication) {
val roleAuthorizer = RoleAuthorizer()
authenticator = ClientCertificateAuthenticator(
roleAuthorizer,
@@ -282,16 +291,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
pipeline.addLast(HttpChunkContentCompressor(1024))
pipeline.addLast(ChunkedWriteHandler())
pipeline.addLast(HttpObjectAggregator(Int.MAX_VALUE))
authenticator?.let{
authenticator?.let {
pipeline.addLast(it)
}
val cacheImplementation = when(val cache = cfg.cache) {
is Configuration.FileSystemCache -> {
FileSystemCache(cache.root, cache.maxAge)
}
else -> throw NotImplementedError()
}
pipeline.addLast(group, ServerHandler(cacheImplementation, cfg.serverPath))
val cacheImplementation = cfg.cache.materialize()
val prefix = Path.of("/").resolve(Path.of(cfg.serverPath ?: "/"))
pipeline.addLast(group, ServerHandler(cacheImplementation, prefix))
pipeline.addLast(ExceptionHandler())
}
}
@@ -305,6 +310,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
}
private val TOO_BIG: FullHttpResponse = DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, Unpooled.EMPTY_BUFFER
).apply {
headers()[HttpHeaderNames.CONTENT_LENGTH] = "0"
}
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
when (cause) {
is DecoderException -> {
@@ -317,6 +328,11 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
}
is ContentTooLargeException -> {
ctx.writeAndFlush(TOO_BIG.retainedDuplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE)
}
else -> {
log.error(cause.message, cause)
ctx.close()
@@ -325,7 +341,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
}
}
private class ServerHandler(private val cache: Cache, private val serverPrefix: String?) :
private class ServerHandler(private val cache: Cache, private val serverPrefix: Path) :
SimpleChannelInboundHandler<FullHttpRequest>() {
companion object {
@@ -335,7 +351,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
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).takeIf(String::isNotEmpty) to uri.substring(i + 1)
return uri.substring(0, i) to uri.substring(i + 1)
}
}
@@ -343,9 +359,12 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val keepAlive: Boolean = HttpUtil.isKeepAlive(msg)
val method = msg.method()
if (method === HttpMethod.GET) {
val (prefix, key) = splitPath(msg)
// val (prefix, key) = splitPath(msg)
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
cache.get(digestString(key.toByteArray()))?.let { channel ->
cache.get(key)?.let { channel ->
log.debug(ctx) {
"Cache hit for key '$key'"
}
@@ -369,6 +388,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
.addListener(ChannelFutureListener.CLOSE)
}
}
else -> {
ctx.write(ChunkedNioStream(channel))
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
@@ -391,13 +411,24 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
ctx.writeAndFlush(response)
}
} else if (method === HttpMethod.PUT) {
val (prefix, key) = splitPath(msg)
val path = Path.of(msg.uri())
val prefix = path.parent
val key = path.fileName.toString()
if (serverPrefix == prefix) {
log.debug(ctx) {
"Added value for key '$key' to build cache"
}
val content = msg.content()
cache.put(digestString(key.toByteArray()), content)
val bodyBytes = msg.content().run {
if (isDirect) {
ByteArray(readableBytes()).also {
readBytes(it)
}
} else {
array()
}
}
cache.put(key, bodyBytes)
val response = DefaultFullHttpResponse(
msg.protocolVersion(), HttpResponseStatus.CREATED,
Unpooled.copiedBuffer(key.toByteArray())
@@ -453,7 +484,7 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
// Create the multithreaded event loops for the server
val bossGroup = NioEventLoopGroup()
val serverSocketChannel = NioServerSocketChannel::class.java
val workerGroup = if (cfg.useVirtualThread) {
val workerGroup = if (cfg.isUseVirtualThread) {
NioEventLoopGroup(0, Executors.newVirtualThreadPerTaskExecutor())
} else {
NioEventLoopGroup(0, Executors.newWorkStealingPool())
@@ -477,8 +508,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
companion object {
private fun String.toUrl() : URL = URL.of(URI(this), null)
private val log by lazy {
contextLogger()
}
@@ -486,9 +515,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
private const val PROTOCOL_HANDLER = "java.protocol.handler.pkgs"
private const val HANDLERS_PACKAGE = "net.woggioni.gbcs.url"
val CONFIGURATION_SCHEMA_URL by lazy {
"classpath:net/woggioni/gbcs/gbcs.xsd".toUrl()
}
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
/**
@@ -515,7 +541,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
fun loadConfiguration(args: Array<String>): Configuration {
// registerUrlProtocolHandler()
URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
// Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader
val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")
@@ -524,6 +549,9 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
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 ->
@@ -533,38 +561,29 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
}
}
// val schemaUrl = javaClass.getResource("/net/woggioni/gbcs/gbcs.xsd")
val schemaUrl = CONFIGURATION_SCHEMA_URL
val dbf = Xml.newDocumentBuilderFactory(schemaUrl)
// val schemaUrl = GBCS.CONFIGURATION_SCHEMA_URL
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().apply {
setErrorHandler(Xml.ErrorHandler(schemaUrl))
}
// dbf.schema = Xml.getSchema(schemaUrl)
val db = dbf.newDocumentBuilder()
val doc = Files.newInputStream(configurationFile).use(db::parse)
return Configuration.parse(doc)
return Parser.parse(doc)
}
@JvmStatic
fun main(args: Array<String>) {
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 {
}
}
fun digest(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): String {
return JWO.bytesToHex(digest(data, md))
}
}
}

View File

@@ -2,87 +2,7 @@ package net.woggioni.gbcs
import io.netty.channel.ChannelHandlerContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.net.InetSocketAddress
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun Logger.traceParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isTraceEnabled) {
val (format, params) = messageBuilder()
trace(format, params)
}
}
inline fun Logger.debugParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isDebugEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.infoParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isInfoEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.warnParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isWarnEnabled) {
val (format, params) = messageBuilder()
warn(format, params)
}
}
inline fun Logger.errorParam(messageBuilder : () -> Pair<String, Array<Any>>) {
if(isErrorEnabled) {
val (format, params) = messageBuilder()
error(format, params)
}
}
inline fun log(log : Logger,
filter : Logger.() -> Boolean,
loggerMethod : Logger.(String) -> Unit, messageBuilder : () -> String) {
if(log.filter()) {
log.loggerMethod(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder : () -> String) {
if(isTraceEnabled) {
trace(messageBuilder())
}
}
inline fun Logger.debug(messageBuilder : () -> String) {
if(isDebugEnabled) {
debug(messageBuilder())
}
}
inline fun Logger.info(messageBuilder : () -> String) {
if(isInfoEnabled) {
info(messageBuilder())
}
}
inline fun Logger.warn(messageBuilder : () -> String) {
if(isWarnEnabled) {
warn(messageBuilder())
}
}
inline fun Logger.error(messageBuilder : () -> String) {
if(isErrorEnabled) {
error(messageBuilder())
}
}
inline fun Logger.trace(ctx : ChannelHandlerContext, messageBuilder : () -> String) {
log(this, ctx, { isTraceEnabled }, { trace(it) } , messageBuilder)
@@ -107,25 +27,4 @@ inline fun log(log : Logger, ctx : ChannelHandlerContext,
val clientAddress = (ctx.channel().remoteAddress() as InetSocketAddress).address.hostAddress
log.loggerMethod(clientAddress + " - " + messageBuilder())
}
}
class LoggingConfig {
init {
val logManager = LogManager.getLogManager()
System.getProperty("log.config.source")?.let withSource@ { source ->
val urls = LoggingConfig::class.java.classLoader.getResources(source)
while(urls.hasMoreElements()) {
val url = urls.nextElement()
url.openStream().use { inputStream ->
logManager.readConfiguration(inputStream)
return@withSource
}
}
Path.of(source).takeIf(Files::exists)
?.let(Files::newInputStream)
?.use(logManager::readConfiguration)
}
}
}

View File

@@ -1,5 +0,0 @@
package net.woggioni.gbcs
enum class Role {
Reader, Writer
}

View File

@@ -2,6 +2,7 @@ package net.woggioni.gbcs
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest
import net.woggioni.gbcs.api.Role
class RoleAuthorizer : Authorizer {

View File

@@ -1,10 +0,0 @@
package net.woggioni.gbcs.cache
import io.netty.buffer.ByteBuf
import java.nio.channels.ByteChannel
interface Cache {
fun get(key : String) : ByteChannel?
fun put(key : String, content : ByteBuf) : Unit
}

View File

@@ -1,20 +1,32 @@
package net.woggioni.gbcs.cache
import io.netty.buffer.ByteBuf
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.digestString
import net.woggioni.gbcs.api.Cache
import net.woggioni.jwo.JWO
import net.woggioni.jwo.LockFile
import java.nio.channels.Channels
import java.nio.channels.FileChannel
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.nio.file.attribute.BasicFileAttributes
import java.security.MessageDigest
import java.time.Duration
import java.time.Instant
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
class FileSystemCache(val root: Path, val maxAge: Duration) : Cache {
class FileSystemCache(
val root: Path,
val maxAge: Duration,
val digestAlgorithm: String?,
val compressionEnabled: Boolean,
val compressionLevel: Int
) : Cache {
private fun lockFilePath(key: String): Path = root.resolve("$key.lock")
@@ -22,40 +34,52 @@ class FileSystemCache(val root: Path, val maxAge: Duration) : Cache {
Files.createDirectories(root)
}
override fun equals(other: Any?): Boolean {
return when (other) {
is FileSystemCache -> {
other.root == root && other.maxAge == maxAge
}
private var nextGc = AtomicReference(Instant.now().plus(maxAge))
else -> false
override fun get(key: String) = (digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
LockFile.acquire(lockFilePath(digest), true).use {
root.resolve(digest).takeIf(Files::exists)?.let { file ->
if (compressionEnabled) {
val inflater = Inflater()
Channels.newChannel(InflaterInputStream(Files.newInputStream(file), inflater))
} else {
FileChannel.open(file, StandardOpenOption.READ)
}
}
}.also {
gc()
}
}
override fun hashCode(): Int {
return root.hashCode() xor maxAge.hashCode()
}
private var nextGc = AtomicReference(Instant.now().plus(maxAge))
override fun get(key: String) = LockFile.acquire(lockFilePath(key), true).use {
root.resolve(key).takeIf(Files::exists)?.let { FileChannel.open(it, StandardOpenOption.READ) }
}.also {
gc()
}
override fun put(key: String, content: ByteBuf) {
LockFile.acquire(lockFilePath(key), false).use {
val file = root.resolve(key)
val tmpFile = Files.createTempFile(root, null, ".tmp")
try {
Files.newOutputStream(tmpFile).use {
content.readBytes(it, content.readableBytes())
override fun put(key: String, content: ByteArray) {
(digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digestString(key.toByteArray(), md)
} ?: key).let { digest ->
LockFile.acquire(lockFilePath(digest), false).use {
val file = root.resolve(digest)
val tmpFile = Files.createTempFile(root, null, ".tmp")
try {
Files.newOutputStream(tmpFile).let {
if (compressionEnabled) {
val deflater = Deflater(compressionLevel)
DeflaterOutputStream(it, deflater)
} else {
it
}
}.use {
it.write(content)
}
Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE)
} catch (t: Throwable) {
Files.delete(tmpFile)
throw t
}
Files.move(tmpFile, file, StandardCopyOption.ATOMIC_MOVE)
} catch (t: Throwable) {
Files.delete(tmpFile)
throw t
}
}.also {
gc()
@@ -87,4 +111,23 @@ class FileSystemCache(val root: Path, val maxAge: Duration) : Cache {
Files.delete(lockFile)
}
}
override fun close() {}
companion object {
fun digest(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest = MessageDigest.getInstance("MD5")
): String {
return JWO.bytesToHex(digest(data, md))
}
}
}

View File

@@ -0,0 +1,27 @@
package net.woggioni.gbcs.cache
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.GBCS
import net.woggioni.jwo.Application
import java.nio.file.Path
import java.time.Duration
data class FileSystemCacheConfiguration(
val root: Path?,
val maxAge: Duration,
val digestAlgorithm : String?,
val compressionEnabled: Boolean,
val compressionLevel: Int,
) : Configuration.Cache {
override fun materialize() = FileSystemCache(
root ?: Application.builder("gbcs").build().computeCacheDirectory(),
maxAge,
digestAlgorithm,
compressionEnabled,
compressionLevel
)
override fun getNamespaceURI() = GBCS.GBCS_NAMESPACE_URI
override fun getTypeName() = "fileSystemCacheType"
}

View File

@@ -0,0 +1,66 @@
package net.woggioni.gbcs.cache
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.Xml
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.nio.file.Path
import java.time.Duration
import java.util.zip.Deflater
class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun getXmlSchemaLocation() = "classpath:net/woggioni/gbcs/schema/gbcs.xsd"
override fun getXmlType() = "fileSystemCacheType"
override fun getXmlNamespace() = "urn:net.woggioni.gbcs"
override fun deserialize(el: Element): FileSystemCacheConfiguration {
val path = el.getAttribute("path")
.takeIf(String::isNotEmpty)
?.let(Path::of)
val maxAge = el.getAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse)
?: Duration.ofDays(1)
val enableCompression = el.getAttribute("enable-compression")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: true
val compressionLevel = el.getAttribute("compression-level")
.takeIf(String::isNotEmpty)
?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) ?: "MD5"
return FileSystemCacheConfiguration(
path,
maxAge,
digestAlgorithm,
enableCompression,
compressionLevel
)
}
override fun serialize(doc: Document, cache : FileSystemCacheConfiguration) = cache.run {
val result = doc.createElement("cache")
Xml.of(doc, result) {
val prefix = doc.lookupPrefix(GBCS.GBCS_NAMESPACE_URI)
attr("xs:type", "${prefix}:fileSystemCacheType", GBCS.XML_SCHEMA_NAMESPACE_URI)
attr("path", root.toString())
attr("max-age", maxAge.toString())
digestAlgorithm?.let { digestAlgorithm ->
attr("digest", digestAlgorithm)
}
attr("enable-compression", compressionEnabled.toString())
compressionLevel.takeIf {
it != Deflater.DEFAULT_COMPRESSION
}?.let {
attr("compression-level", it.toString())
}
}
result
}
}

View File

@@ -0,0 +1,15 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.Configuration
import java.util.ServiceLoader
object CacheSerializers {
val index = (Configuration::class.java.module.layer?.let { layer ->
ServiceLoader.load(layer, CacheProvider::class.java)
} ?: ServiceLoader.load(CacheProvider::class.java))
.asSequence()
.map {
(it.xmlNamespace to it.xmlType) to it
}.toMap()
}

View File

@@ -1,14 +1,11 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.Role
import net.woggioni.gbcs.Xml.Companion.asIterable
import org.w3c.dom.Document
import org.w3c.dom.Element
import net.woggioni.gbcs.api.Role
import java.nio.file.Path
import java.nio.file.Paths
import java.security.cert.X509Certificate
import java.time.Duration
@ConsistentCopyVisibility
data class Configuration private constructor(
val host: String,
val port: Int,
@@ -36,10 +33,6 @@ data class Configuration private constructor(
get() = groups.asSequence().flatMap { it.roles }.toSet()
}
data class HostAndPort(val host: String, val port: Int) {
override fun toString() = "$host:$port"
}
fun interface UserExtractor {
fun extract(cert :X509Certificate) : User
}
@@ -107,188 +100,210 @@ data class Configuration private constructor(
useVirtualThread
)
fun parse(document: Document): Configuration {
val root = document.documentElement
var cache: Cache? = null
var host = "127.0.0.1"
var port = 11080
var users = emptyMap<String, User>()
var groups = emptyMap<String, Group>()
var tls: Tls? = null
val serverPath = root.getAttribute("path")
val useVirtualThread = root.getAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var authentication : Authentication? = null
for (child in root.asIterable()) {
when (child.nodeName) {
"authorization" -> {
for (gchild in child.asIterable()) {
when (child.nodeName) {
"users" -> {
users = parseUsers(child)
}
"groups" -> {
val pair = parseGroups(child, users)
users = pair.first
groups = pair.second
}
}
}
}
"bind" -> {
host = child.getAttribute("host")
port = Integer.parseInt(child.getAttribute("port"))
}
"cache" -> {
for (gchild in child.asIterable()) {
when (gchild.nodeName) {
"file-system-cache" -> {
val cacheFolder = gchild.getAttribute("path")
.takeIf(String::isNotEmpty)
?.let(Paths::get)
?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
val maxAge = gchild.getAttribute("max-age")
.takeIf(String::isNotEmpty)
?.let(Duration::parse)
?: Duration.ofDays(1)
cache = FileSystemCache(cacheFolder, maxAge)
}
}
}
}
"authentication" -> {
for (gchild in child.asIterable()) {
when (gchild.nodeName) {
"basic" -> {
authentication = BasicAuthentication()
}
"client-certificate" -> {
var tlsExtractorUser : TlsCertificateExtractor? = null
var tlsExtractorGroup : TlsCertificateExtractor? = null
for (gchild in child.asIterable()) {
when (gchild.nodeName) {
"group-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
}
"user-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
}
}
}
authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
}
}
}
}
"tls" -> {
val verifyClients = child.getAttribute("verify-clients")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var keyStore: KeyStore? = null
var trustStore: TrustStore? = null
for (granChild in child.asIterable()) {
when (granChild.nodeName) {
"keystore" -> {
val keyStoreFile = Paths.get(granChild.getAttribute("file"))
val keyStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty)
val keyAlias = granChild.getAttribute("key-alias")
val keyPassword = granChild.getAttribute("key-password")
.takeIf(String::isNotEmpty)
keyStore = KeyStore(
keyStoreFile,
keyStorePassword,
keyAlias,
keyPassword
)
}
"truststore" -> {
val trustStoreFile = Paths.get(granChild.getAttribute("file"))
val trustStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty)
val checkCertificateStatus = granChild.getAttribute("check-certificate-status")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: false
trustStore = TrustStore(
trustStoreFile,
trustStorePassword,
checkCertificateStatus
)
}
}
}
tls = Tls(keyStore, trustStore, verifyClients)
}
}
}
return of(host, port, serverPath, users, groups, cache!!, authentication, tls, useVirtualThread)
}
private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
when (it.nodeName) {
"reader" -> Role.Reader
"writer" -> Role.Writer
else -> throw UnsupportedOperationException("Illegal node '${it.nodeName}'")
}
}.toSet()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter {
it.nodeName == "user"
}.map {
it.getAttribute("ref")
}.toSet()
private fun parseUsers(root: Element): Map<String, User> {
return root.asIterable().asSequence().filter {
it.nodeName == "user"
}.map { el ->
val username = el.getAttribute("name")
val password = el.getAttribute("password").takeIf(String::isNotEmpty)
username to User(username, password, emptySet())
}.toMap()
}
private fun parseGroups(root: Element, knownUsers : Map<String, User>): Pair<Map<String, User>, Map<String, Group>> {
val userGroups = mutableMapOf<String, MutableSet<String>>()
val groups = root.asIterable().asSequence().filter {
it.nodeName == "group"
}.map { el ->
val groupName = el.getAttribute("name")
var roles = emptySet<Role>()
for (child in el.asIterable()) {
when (child.nodeName) {
"users" -> {
parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user ->
userGroups.computeIfAbsent(user.name) {
mutableSetOf()
}.add(groupName)
}
}
"roles" -> {
roles = parseRoles(child)
}
}
}
groupName to Group(groupName, roles)
}.toMap()
val users = knownUsers.map { (name, user) ->
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
}.toMap()
return users to groups
}
// fun parse(document: Document): Configuration {
// val cacheSerializers = ServiceLoader.load(Configuration::class.java.module.layer, CacheSerializer::class.java)
// .asSequence()
// .map {
// "${it.xmlType}:${it.xmlNamespace}" to it
// }.toMap()
// val root = document.documentElement
// var cache: Cache? = null
// var host = "127.0.0.1"
// var port = 11080
// var users = emptyMap<String, User>()
// var groups = emptyMap<String, Group>()
// var tls: Tls? = null
// val serverPath = root.getAttribute("path")
// val useVirtualThread = root.getAttribute("useVirtualThreads")
// .takeIf(String::isNotEmpty)
// ?.let(String::toBoolean) ?: false
// var authentication : Authentication? = null
// for (child in root.asIterable()) {
// when (child.nodeName) {
// "authorization" -> {
// for (gchild in child.asIterable()) {
// when (child.nodeName) {
// "users" -> {
// users = parseUsers(child)
// }
//
// "groups" -> {
// val pair = parseGroups(child, users)
// users = pair.first
// groups = pair.second
// }
// }
// }
// }
//
// "bind" -> {
// host = child.getAttribute("host")
// port = Integer.parseInt(child.getAttribute("port"))
// }
//
// "cache" -> {
// val type = child.getAttribute("xs:type")
// val serializer = cacheSerializers.get(type) ?: throw NotImplementedError()
// cache = serializer.deserialize(child)
//
// when(child.getAttribute("xs:type")) {
// "gbcs:fileSystemCacheType" -> {
// val cacheFolder = child.getAttribute("path")
// .takeIf(String::isNotEmpty)
// ?.let(Paths::get)
// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
// val maxAge = child.getAttribute("max-age")
// .takeIf(String::isNotEmpty)
// ?.let(Duration::parse)
// ?: Duration.ofDays(1)
// cache = FileSystemCache(cacheFolder, maxAge)
// }
// }
//// for (gchild in child.asIterable()) {
//// when (gchild.nodeName) {
//// "file-system-cache" -> {
//// val cacheFolder = gchild.getAttribute("path")
//// .takeIf(String::isNotEmpty)
//// ?.let(Paths::get)
//// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
//// val maxAge = gchild.getAttribute("max-age")
//// .takeIf(String::isNotEmpty)
//// ?.let(Duration::parse)
//// ?: Duration.ofDays(1)
//// cache = FileSystemCache(cacheFolder, maxAge)
//// }
//// }
//// }
// }
//
// "authentication" -> {
// for (gchild in child.asIterable()) {
// when (gchild.nodeName) {
// "basic" -> {
// authentication = BasicAuthentication()
// }
//
// "client-certificate" -> {
// var tlsExtractorUser : TlsCertificateExtractor? = null
// var tlsExtractorGroup : TlsCertificateExtractor? = null
// for (gchild in child.asIterable()) {
// when (gchild.nodeName) {
// "group-extractor" -> {
// val attrName = gchild.getAttribute("attribute-name")
// val pattern = gchild.getAttribute("pattern")
// tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
// }
//
// "user-extractor" -> {
// val attrName = gchild.getAttribute("attribute-name")
// val pattern = gchild.getAttribute("pattern")
// tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
// }
// }
// }
// authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
// }
// }
// }
// }
//
// "tls" -> {
// val verifyClients = child.getAttribute("verify-clients")
// .takeIf(String::isNotEmpty)
// ?.let(String::toBoolean) ?: false
// var keyStore: KeyStore? = null
// var trustStore: TrustStore? = null
// for (granChild in child.asIterable()) {
// when (granChild.nodeName) {
// "keystore" -> {
// val keyStoreFile = Paths.get(granChild.getAttribute("file"))
// val keyStorePassword = granChild.getAttribute("password")
// .takeIf(String::isNotEmpty)
// val keyAlias = granChild.getAttribute("key-alias")
// val keyPassword = granChild.getAttribute("key-password")
// .takeIf(String::isNotEmpty)
// keyStore = KeyStore(
// keyStoreFile,
// keyStorePassword,
// keyAlias,
// keyPassword
// )
// }
//
// "truststore" -> {
// val trustStoreFile = Paths.get(granChild.getAttribute("file"))
// val trustStorePassword = granChild.getAttribute("password")
// .takeIf(String::isNotEmpty)
// val checkCertificateStatus = granChild.getAttribute("check-certificate-status")
// .takeIf(String::isNotEmpty)
// ?.let(String::toBoolean)
// ?: false
// trustStore = TrustStore(
// trustStoreFile,
// trustStorePassword,
// checkCertificateStatus
// )
// }
// }
// }
// tls = Tls(keyStore, trustStore, verifyClients)
// }
// }
// }
// return of(host, port, serverPath, users, groups, cache!!, authentication, tls, useVirtualThread)
// }
//
// private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
// when (it.nodeName) {
// "reader" -> Role.Reader
// "writer" -> Role.Writer
// else -> throw UnsupportedOperationException("Illegal node '${it.nodeName}'")
// }
// }.toSet()
//
// private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter {
// it.nodeName == "user"
// }.map {
// it.getAttribute("ref")
// }.toSet()
//
// private fun parseUsers(root: Element): Map<String, User> {
// return root.asIterable().asSequence().filter {
// it.nodeName == "user"
// }.map { el ->
// val username = el.getAttribute("name")
// val password = el.getAttribute("password").takeIf(String::isNotEmpty)
// username to User(username, password, emptySet())
// }.toMap()
// }
//
// private fun parseGroups(root: Element, knownUsers : Map<String, User>): Pair<Map<String, User>, Map<String, Group>> {
// val userGroups = mutableMapOf<String, MutableSet<String>>()
// val groups = root.asIterable().asSequence().filter {
// it.nodeName == "group"
// }.map { el ->
// val groupName = el.getAttribute("name")
// var roles = emptySet<Role>()
// for (child in el.asIterable()) {
// when (child.nodeName) {
// "users" -> {
// parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user ->
// userGroups.computeIfAbsent(user.name) {
// mutableSetOf()
// }.add(groupName)
// }
// }
// "roles" -> {
// roles = parseRoles(child)
// }
// }
// }
// groupName to Group(groupName, roles)
// }.toMap()
// val users = knownUsers.map { (name, user) ->
// name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
// }.toMap()
// return users to groups
// }
}
}

View File

@@ -0,0 +1,231 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Configuration.Authentication
import net.woggioni.gbcs.api.Configuration.BasicAuthentication
import net.woggioni.gbcs.api.Configuration.Cache
import net.woggioni.gbcs.api.Configuration.ClientCertificateAuthentication
import net.woggioni.gbcs.api.Configuration.Group
import net.woggioni.gbcs.api.Configuration.KeyStore
import net.woggioni.gbcs.api.Configuration.Tls
import net.woggioni.gbcs.api.Configuration.TlsCertificateExtractor
import net.woggioni.gbcs.api.Configuration.TrustStore
import net.woggioni.gbcs.api.Configuration.User
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.TypeInfo
import java.nio.file.Paths
object Parser {
fun parse(document: Document): Configuration {
val root = document.documentElement
var cache: Cache? = null
var host = "127.0.0.1"
var port = 11080
var users = emptyMap<String, User>()
var groups = emptyMap<String, Group>()
var tls: Tls? = null
val serverPath = root.getAttribute("path")
val useVirtualThread = root.getAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var authentication: Authentication? = null
for (child in root.asIterable()) {
when (child.localName) {
"authorization" -> {
for (gchild in child.asIterable()) {
when (child.localName) {
"users" -> {
users = parseUsers(child)
}
"groups" -> {
val pair = parseGroups(child, users)
users = pair.first
groups = pair.second
}
}
}
}
"bind" -> {
host = child.getAttribute("host")
port = Integer.parseInt(child.getAttribute("port"))
}
"cache" -> {
// val type = child.getAttribute("xs:type").split(":")
// val namespaceURI = child.lookupNamespaceURI(type[0])
// val typeName = type[1]
cache = (child as? TypeInfo)?.let { tf ->
val typeNamespace = tf.typeNamespace
val typeName = tf.typeName
CacheSerializers.index[typeNamespace to typeName]
}?.deserialize(child) ?: throw NotImplementedError()
// cache = serializer.deserialize(child)
// when(child.getAttribute("xs:type")) {
// "gbcs:fileSystemCacheType" -> {
// val cacheFolder = child.getAttribute("path")
// .takeIf(String::isNotEmpty)
// ?.let(Paths::get)
// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
// val maxAge = child.getAttribute("max-age")
// .takeIf(String::isNotEmpty)
// ?.let(Duration::parse)
// ?: Duration.ofDays(1)
// cache = FileSystemCache(cacheFolder, maxAge)
// }
// }
// for (gchild in child.asIterable()) {
// when (gchild.localName) {
// "file-system-cache" -> {
// val cacheFolder = gchild.getAttribute("path")
// .takeIf(String::isNotEmpty)
// ?.let(Paths::get)
// ?: Paths.get(System.getProperty("user.home")).resolve(".gbcs")
// val maxAge = gchild.getAttribute("max-age")
// .takeIf(String::isNotEmpty)
// ?.let(Duration::parse)
// ?: Duration.ofDays(1)
// cache = FileSystemCache(cacheFolder, maxAge)
// }
// }
// }
}
"authentication" -> {
for (gchild in child.asIterable()) {
when (gchild.localName) {
"basic" -> {
authentication = BasicAuthentication()
}
"client-certificate" -> {
var tlsExtractorUser: TlsCertificateExtractor? = null
var tlsExtractorGroup: TlsCertificateExtractor? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"group-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
}
"user-extractor" -> {
val attrName = gchild.getAttribute("attribute-name")
val pattern = gchild.getAttribute("pattern")
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
}
}
}
authentication = ClientCertificateAuthentication(tlsExtractorUser, tlsExtractorGroup)
}
}
}
}
"tls" -> {
val verifyClients = child.getAttribute("verify-clients")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean) ?: false
var keyStore: KeyStore? = null
var trustStore: TrustStore? = null
for (granChild in child.asIterable()) {
when (granChild.localName) {
"keystore" -> {
val keyStoreFile = Paths.get(granChild.getAttribute("file"))
val keyStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty)
val keyAlias = granChild.getAttribute("key-alias")
val keyPassword = granChild.getAttribute("key-password")
.takeIf(String::isNotEmpty)
keyStore = KeyStore(
keyStoreFile,
keyStorePassword,
keyAlias,
keyPassword
)
}
"truststore" -> {
val trustStoreFile = Paths.get(granChild.getAttribute("file"))
val trustStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty)
val checkCertificateStatus = granChild.getAttribute("check-certificate-status")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: false
trustStore = TrustStore(
trustStoreFile,
trustStorePassword,
checkCertificateStatus
)
}
}
}
tls = Tls(keyStore, trustStore, verifyClients)
}
}
}
return Configuration(host, port, serverPath, users, groups, cache!!, authentication, tls, useVirtualThread)
}
private fun parseRoles(root: Element) = root.asIterable().asSequence().map {
when (it.localName) {
"reader" -> Role.Reader
"writer" -> Role.Writer
else -> throw UnsupportedOperationException("Illegal node '${it.localName}'")
}
}.toSet()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().filter {
it.localName == "user"
}.map {
it.getAttribute("ref")
}.toSet()
private fun parseUsers(root: Element): Map<String, User> {
return root.asIterable().asSequence().filter {
it.localName == "user"
}.map { el ->
val username = el.getAttribute("name")
val password = el.getAttribute("password").takeIf(String::isNotEmpty)
username to User(username, password, emptySet())
}.toMap()
}
private fun parseGroups(root: Element, knownUsers: Map<String, User>): Pair<Map<String, User>, Map<String, Group>> {
val userGroups = mutableMapOf<String, MutableSet<String>>()
val groups = root.asIterable().asSequence().filter {
it.localName == "group"
}.map { el ->
val groupName = el.getAttribute("name")
var roles = emptySet<Role>()
for (child in el.asIterable()) {
when (child.localName) {
"users" -> {
parseUserRefs(child).mapNotNull(knownUsers::get).forEach { user ->
userGroups.computeIfAbsent(user.name) {
mutableSetOf()
}.add(groupName)
}
}
"roles" -> {
roles = parseRoles(child)
}
}
}
groupName to Group(groupName, roles)
}.toMap()
val users = knownUsers.map { (name, user) ->
name to User(name, user.password, userGroups[name]?.mapNotNull { groups[it] }?.toSet() ?: emptySet())
}.toMap()
return users to groups
}
}

View File

@@ -1,34 +1,37 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.Xml
import org.w3c.dom.Document
object Serializer {
private const val GBCS_NAMESPACE: String = "urn:net.woggioni.gbcs"
private const val GBCS_PREFIX: String = "gbcs"
fun serialize(conf : Configuration) : Document {
return Xml.of(GBCS_NAMESPACE, GBCS_PREFIX + ":server") {
attr("userVirtualThreads", conf.useVirtualThread.toString())
conf.serverPath?.let { serverPath ->
val schemaLocations = CacheSerializers.index.values.asSequence().map {
it.xmlNamespace to it.xmlSchemaLocation
}.toMap()
return Xml.of(GBCS.GBCS_NAMESPACE_URI, GBCS.GBCS_PREFIX + ":server") {
attr("useVirtualThreads", conf.isUseVirtualThread.toString())
// attr("xmlns:xs", GradleBuildCacheServer.XML_SCHEMA_NAMESPACE_URI)
val value = schemaLocations.asSequence().map { (k, v) -> "$k $v" }.joinToString(" ")
attr("xs:schemaLocation",value , namespaceURI = GBCS.XML_SCHEMA_NAMESPACE_URI)
conf.serverPath
?.takeIf(String::isNotEmpty)
?.let { serverPath ->
attr("path", serverPath)
}
node("bind") {
attr("host", conf.host)
attr("port", conf.port.toString())
}
node("cache") {
when(val cache = conf.cache) {
is Configuration.FileSystemCache -> {
node("file-system-cache") {
attr("path", cache.root.toString())
attr("max-age", cache.maxAge.toString())
}
}
else -> throw NotImplementedError()
}
}
val cache = conf.cache
val serializer : CacheProvider<Configuration.Cache> =
(CacheSerializers.index[cache.namespaceURI to cache.typeName] as? CacheProvider<Configuration.Cache>) ?: throw NotImplementedError()
element.appendChild(serializer.serialize(doc, cache))
node("authorization") {
node("users") {
for(user in conf.users.values) {
@@ -112,7 +115,7 @@ object Serializer {
trustStore.password?.let { password ->
attr("password", password)
}
attr("check-certificate-status", trustStore.checkCertificateStatus.toString())
attr("check-certificate-status", trustStore.isCheckCertificateStatus.toString())
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema targetNamespace="urn:net.woggioni.gbcs"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:gbcs="urn:net.woggioni.gbcs">
xmlns:gbcs="urn:net.woggioni.gbcs"
elementFormDefault="unqualified"
>
<xs:element name="server" type="gbcs:serverType"/>
<xs:complexType name="serverType">
@@ -21,7 +23,7 @@
<xs:field xpath="@ref"/>
</xs:keyref>
</xs:element>
<xs:element name="authentication" type="gbcs:authenticationType" maxOccurs="1"/>
<xs:element name="authentication" type="gbcs:authenticationType" minOccurs="0" maxOccurs="1"/>
<xs:element name="tls-certificate-authorization" type="gbcs:tlsCertificateAuthorizationType" minOccurs="0" maxOccurs="1"/>
<xs:element name="tls" type="gbcs:tlsType" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
@@ -34,15 +36,18 @@
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
</xs:complexType>
<xs:complexType name="cacheType">
<xs:choice>
<xs:element name="file-system-cache" type="gbcs:fileSystemCacheType"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="cacheType" abstract="true"/>
<xs:complexType name="fileSystemCacheType">
<xs:attribute name="path" type="xs:string" use="required"/>
<xs:attribute name="max-age" type="xs:string" default="P1D"/>
<xs:complexContent>
<xs:extension base="gbcs:cacheType">
<xs:attribute name="path" type="xs:string" use="required"/>
<xs:attribute name="max-age" type="xs:duration" default="P1D"/>
<xs:attribute name="digest" type="xs:token" default="MD5"/>
<xs:attribute name="enable-compression" type="xs:boolean" default="true"/>
<xs:attribute name="compression-level" type="xs:byte" default="-1"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="tlsCertificateAuthorizationType">

View File

@@ -1,7 +0,0 @@
module net.woggioni.gbcs.test {
requires org.junit.jupiter.api;
requires net.woggioni.gbcs;
requires kotlin.stdlib;
requires java.xml;
requires java.naming;
}

View File

@@ -9,16 +9,13 @@ import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectAltPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;

View File

@@ -1,7 +1,7 @@
package net.woggioni.gbcs.test
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.api.Configuration
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.ClassOrderer
@@ -14,7 +14,7 @@ import java.nio.file.Path
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
abstract class AbstractServerTest {
abstract class AbstractServerTestKt {
protected lateinit var cfg : Configuration

View File

@@ -1,12 +1,11 @@
package net.woggioni.gbcs.test
import io.netty.handler.codec.Headers
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.AbstractNettyHttpAuthenticator.Companion.hashPassword
import net.woggioni.gbcs.Authorizer
import net.woggioni.gbcs.Role
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
@@ -20,10 +19,11 @@ import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.time.Duration
import java.util.Base64
import java.util.zip.Deflater
import kotlin.random.Random
class BasicAuthServerTest : AbstractServerTest() {
class BasicAuthServerTestKt : AbstractServerTestKt() {
companion object {
private const val PASSWORD = "password"
@@ -33,25 +33,31 @@ class BasicAuthServerTest : AbstractServerTest() {
private val random = Random(101325)
private val keyValuePair = newEntry(random)
private val serverPath = "gbcs"
override fun setUp() {
this.cacheDir = testDir.resolve("cache")
val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
cfg = Configuration.of(
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
host = "127.0.0.1",
port = ServerSocket(0).localPort + 1,
users = listOf(
cfg = Configuration(
"127.0.0.1",
ServerSocket(0).localPort + 1,
serverPath,
listOf(
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)),
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)),
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup))
).asSequence().map { it.name to it}.toMap(),
groups = sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
authentication = Configuration.BasicAuthentication(),
useVirtualThread = true,
tls = null,
serverPath = "/"
sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
FileSystemCacheConfiguration(this.cacheDir,
maxAge = Duration.ofSeconds(3600 * 24),
digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION,
compressionEnabled = false
),
Configuration.BasicAuthentication(),
null,
true,
)
Xml.write(Serializer.serialize(cfg), System.out)
}
@@ -67,7 +73,7 @@ class BasicAuthServerTest : AbstractServerTest() {
}
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/$key"))
.uri(URI.create("http://${cfg.host}:${cfg.port}/$serverPath/$key"))
fun newEntry(random : Random) : Pair<String, ByteArray> {

View File

@@ -1,32 +1,44 @@
package net.woggioni.gbcs.test
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.base.GBCS.toUrl
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.configuration.Parser
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
class ConfigurationTest {
class ConfigurationTestKt {
@Test
fun test(@TempDir testDir : Path) {
URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
val dbf = Xml.newDocumentBuilderFactory(GradleBuildCacheServer.CONFIGURATION_SCHEMA_URL)
val db = dbf.newDocumentBuilder()
val configurationUrl = GradleBuildCacheServer.DEFAULT_CONFIGURATION_URL
val doc = configurationUrl.openStream().use(db::parse)
val cfg = Configuration.parse(doc)
// companion object {
// init {
// URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
// }
// }
@ValueSource(
strings = [
"classpath:net/woggioni/gbcs/test/gbcs-default.xml",
"classpath:net/woggioni/gbcs/test/gbcs-memcached.xml",
]
)
@ParameterizedTest
fun test(configurationUrl: String, @TempDir testDir: Path) {
ClasspathUrlStreamHandlerFactoryProvider.install()
val doc = Xml.parseXml(configurationUrl.toUrl())
val cfg = Parser.parse(doc)
val configFile = testDir.resolve("gbcs.xml")
Files.newOutputStream(configFile).use {
Xml.write(Serializer.serialize(cfg), it)
}
val parsed = Configuration.parse(Xml.parseXml(configFile.toUri().toURL()))
Xml.write(Serializer.serialize(cfg), System.out)
val parsed = Parser.parse(Xml.parseXml(configFile.toUri().toURL()))
Assertions.assertEquals(cfg, parsed)
}
}

View File

@@ -1,8 +1,9 @@
package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Order
@@ -15,28 +16,36 @@ import java.net.http.HttpResponse
import java.nio.file.Path
import java.time.Duration
import java.util.Base64
import java.util.zip.Deflater
import kotlin.random.Random
class NoAuthServerTest : AbstractServerTest() {
class NoAuthServerTestKt : AbstractServerTestKt() {
private lateinit var cacheDir : Path
private val random = Random(101325)
private val keyValuePair = newEntry(random)
private val serverPath = "/some/nested/path"
override fun setUp() {
this.cacheDir = testDir.resolve("cache")
cfg = Configuration.of(
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
host = "127.0.0.1",
port = ServerSocket(0).localPort + 1,
users = emptyMap(),
groups = emptyMap(),
authentication = null,
useVirtualThread = true,
tls = null,
serverPath = "/"
cfg = Configuration(
"127.0.0.1",
ServerSocket(0).localPort + 1,
serverPath,
emptyMap(),
emptyMap(),
FileSystemCacheConfiguration(
this.cacheDir,
maxAge = Duration.ofSeconds(3600 * 24),
compressionEnabled = true,
digestAlgorithm = "MD5",
compressionLevel = Deflater.DEFAULT_COMPRESSION
),
null,
null,
true,
)
Xml.write(Serializer.serialize(cfg), System.out)
}
@@ -45,7 +54,7 @@ class NoAuthServerTest : AbstractServerTest() {
}
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
.uri(URI.create("http://${cfg.host}:${cfg.port}/$key"))
.uri(URI.create("http://${cfg.host}:${cfg.port}/$serverPath/$key"))
fun newEntry(random : Random) : Pair<String, ByteArray> {
val key = ByteArray(0x10).let {

View File

@@ -1,9 +1,10 @@
package net.woggioni.gbcs.test
import io.netty.handler.codec.http.HttpResponseStatus
import net.woggioni.gbcs.Role
import net.woggioni.gbcs.Xml
import net.woggioni.gbcs.configuration.Configuration
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.cache.FileSystemCacheConfiguration
import net.woggioni.gbcs.configuration.Serializer
import net.woggioni.gbcs.utils.CertificateUtils
import net.woggioni.gbcs.utils.CertificateUtils.X509Credentials
@@ -23,19 +24,23 @@ import java.security.KeyStore
import java.security.KeyStore.PasswordProtection
import java.time.Duration
import java.util.Base64
import java.util.zip.Deflater
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import kotlin.random.Random
class TlsServerTest : AbstractServerTest() {
class TlsServerTestKt : AbstractServerTestKt() {
companion object {
private const val CA_CERTIFICATE_ENTRY = "gbcs-ca"
private const val CLIENT_CERTIFICATE_ENTRY = "gbcs-client"
private const val SERVER_CERTIFICATE_ENTRY = "gbcs-server"
private const val PASSWORD = "password"
// private fun stripLeadingSlash(s : String) = Path.of("/").root.relativize(Path.of(s).normalize()).toString()
}
private lateinit var cacheDir: Path
@@ -51,6 +56,7 @@ class TlsServerTest : AbstractServerTest() {
private val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
private val random = Random(101325)
private val keyValuePair = newEntry(random)
private val serverPath : String? = null
private val users = listOf(
Configuration.User("user1", null, setOf(readersGroup)),
@@ -104,7 +110,7 @@ class TlsServerTest : AbstractServerTest() {
}
}
fun getClientKeyStore(ca : X509Credentials, subject: X500Name) = KeyStore.getInstance("PKCS12").apply {
fun getClientKeyStore(ca: X509Credentials, subject: X500Name) = KeyStore.getInstance("PKCS12").apply {
val clientCert = CertificateUtils.createClientCertificate(ca, subject, 30)
load(null, null)
@@ -116,7 +122,7 @@ class TlsServerTest : AbstractServerTest() {
)
}
fun getHttpClient(clientKeyStore : KeyStore?): HttpClient {
fun getHttpClient(clientKeyStore: KeyStore?): HttpClient {
val kmf = clientKeyStore?.let {
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
init(it, PASSWORD.toCharArray())
@@ -141,23 +147,28 @@ class TlsServerTest : AbstractServerTest() {
this.trustStoreFile = testDir.resolve("truststore.p12")
this.cacheDir = testDir.resolve("cache")
createKeyStoreAndTrustStore()
cfg = Configuration.of(
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
host = "127.0.0.1",
port = ServerSocket(0).localPort + 1,
users = users.asSequence().map { it.name to it }.toMap(),
groups = sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
authentication = Configuration.ClientCertificateAuthentication(
userExtractor = Configuration.TlsCertificateExtractor("CN", "(.*)"),
groupExtractor = null
cfg = Configuration(
"127.0.0.1",
ServerSocket(0).localPort + 1,
serverPath,
users.asSequence().map { it.name to it }.toMap(),
sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
FileSystemCacheConfiguration(this.cacheDir,
maxAge = Duration.ofSeconds(3600 * 24),
compressionEnabled = true,
compressionLevel = Deflater.DEFAULT_COMPRESSION,
digestAlgorithm = "MD5"
),
useVirtualThread = true,
tls = Configuration.Tls(
Configuration.ClientCertificateAuthentication(
Configuration.TlsCertificateExtractor("CN", "(.*)"),
null
),
Configuration.Tls(
Configuration.KeyStore(this.serverKeyStoreFile, null, SERVER_CERTIFICATE_ENTRY, PASSWORD),
Configuration.TrustStore(this.trustStoreFile, null, false),
true
),
serverPath = "/"
false,
)
Xml.write(Serializer.serialize(cfg), System.out)
}
@@ -166,7 +177,7 @@ class TlsServerTest : AbstractServerTest() {
}
fun newRequestBuilder(key: String) = HttpRequest.newBuilder()
.uri(URI.create("https://${cfg.host}:${cfg.port}/$key"))
.uri(URI.create("https://${cfg.host}:${cfg.port}/${serverPath ?: ""}/$key"))
fun buildAuthorizationHeader(user: Configuration.User, password: String): String {
val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let {

View File

@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import javax.naming.ldap.LdapName
class X500NameTest {
class X500NameTestKt {
@Test
fun test() {

View File

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

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gbcs:server useVirtualThreads="false" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
xmlns:gbcs="urn:net.woggioni.gbcs"
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">
<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">
<server host="127.0.0.1" port="11211"/>
</cache>
<authentication>
<none/>
</authentication>
</gbcs:server>