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

@@ -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,262 +0,0 @@
package net.woggioni.gbcs
import org.slf4j.LoggerFactory
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.SAXNotRecognizedException
import org.xml.sax.SAXNotSupportedException
import org.xml.sax.SAXParseException
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING
import javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.Schema
import javax.xml.validation.SchemaFactory
import org.xml.sax.ErrorHandler as ErrHandler
class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> {
private var cursor: Int = 0
override fun hasNext(): Boolean {
return cursor < nodeList.length
}
override fun next(): Node {
return if (hasNext()) nodeList.item(cursor++) else throw NoSuchElementException()
}
}
class ElementIterator(parent: Element, name: String? = null) : Iterator<Element> {
private val it: NodeListIterator
private val name: String?
private var next: Element?
init {
it = NodeListIterator(parent.childNodes)
this.name = name
next = getNext()
}
override fun hasNext(): Boolean {
return next != null
}
override fun next(): Element {
val result = next ?: throw NoSuchElementException()
next = getNext()
return result
}
private fun getNext(): Element? {
var result: Element? = null
while (it.hasNext()) {
val node: Node = it.next()
if (node is Element && (name == null || name == node.tagName)) {
result = node
break
}
}
return result
}
}
class Xml(private val doc: Document, val element: Element) {
class ErrorHandler(private val fileURL: URL) : ErrHandler {
companion object {
private val log = LoggerFactory.getLogger(ErrorHandler::class.java)
}
override fun warning(ex: SAXParseException) {
log.warn(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
}
override fun error(ex: SAXParseException) {
log.error(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
throw ex
}
override fun fatalError(ex: SAXParseException) {
log.error(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
throw ex
}
}
companion object {
fun Element.asIterable() = Iterable { ElementIterator(this, null) }
fun NodeList.asIterable() = Iterable { NodeListIterator(this) }
private fun disableProperty(dbf: DocumentBuilderFactory, propertyName: String) {
try {
dbf.setAttribute(propertyName, "")
} catch (iae: IllegalArgumentException) {
// Property not supported.
}
}
private fun disableProperty(sf: SchemaFactory, propertyName: String) {
try {
sf.setProperty(propertyName, "")
} catch (ex: SAXNotRecognizedException) {
// Property not supported.
} catch (ex: SAXNotSupportedException) {
}
}
fun getSchema(schema: URL): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, true)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD)
sf.errorHandler = ErrorHandler(schema)
return sf.newSchema(schema)
}
fun getSchema(inputStream: InputStream): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, true)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD)
return sf.newSchema(StreamSource(inputStream))
}
fun newDocumentBuilderFactory(schemaResourceURL: URL?): DocumentBuilderFactory {
val dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature(FEATURE_SECURE_PROCESSING, true)
disableProperty(dbf, ACCESS_EXTERNAL_SCHEMA)
disableProperty(dbf, ACCESS_EXTERNAL_DTD)
dbf.isExpandEntityReferences = false
dbf.isIgnoringComments = true
dbf.isNamespaceAware = true
dbf.isValidating = false
schemaResourceURL?.let {
dbf.schema = getSchema(it)
}
return dbf
}
fun newDocumentBuilder(resource: URL, schemaResourceURL: URL?): DocumentBuilder {
val db = newDocumentBuilderFactory(schemaResourceURL).newDocumentBuilder()
db.setErrorHandler(ErrorHandler(resource))
return db
}
fun parseXmlResource(resource: URL, schemaResourceURL: URL?): Document {
val db = newDocumentBuilder(resource, schemaResourceURL)
return resource.openStream().use(db::parse)
}
fun parseXml(sourceURL : URL, sourceStream: InputStream? = null, schemaResourceURL: URL? = null): Document {
val db = newDocumentBuilder(sourceURL, schemaResourceURL)
return sourceStream?.let(db::parse) ?: sourceURL.openStream().use(db::parse)
}
//
// fun newDocumentBuilder(resource: URL): DocumentBuilder {
// val db = newDocumentBuilderFactory(null).newDocumentBuilder()
// db.setErrorHandler(XmlErrorHandler(resource))
// return db
// }
// fun parseXmlResource(resource: URL): Document {
// val db = newDocumentBuilder(resource, null)
// return resource.openStream().use(db::parse)
// }
fun write(doc: Document, output: OutputStream) {
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
// val domImpl = doc.getImplementation()
// val docType = domImpl.createDocumentType(
// "plist",
// "-//Apple//DTD PLIST 1.0//EN",
// "http://www.apple.com/DTDs/PropertyList-1.0.dtd"
// )
// transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, docType.getPublicId())
// transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, docType.getSystemId())
// val transformerFactory = TransformerFactory.newInstance()
// val transformer: Transformer = transformerFactory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes")
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
val source = DOMSource(doc)
val result = StreamResult(output)
transformer.transform(source, result)
}
fun of(namespaceURI: String, qualifiedName: String, schemaResourceURL: URL? = null, cb: Xml.(el: Element) -> Unit): Document {
val dbf = newDocumentBuilderFactory(schemaResourceURL)
val db = dbf.newDocumentBuilder()
val doc = db.newDocument()
val root = doc.createElementNS(namespaceURI, qualifiedName)
.also(doc::appendChild)
Xml(doc, root).cb(root)
return doc
}
fun of(doc: Document, el: Element, cb: Xml.(el: Element) -> Unit): Element {
Xml(doc, el).cb(el)
return el
}
fun Element.removeChildren() {
while (true) {
removeChild(firstChild ?: break)
}
}
}
fun node(
name: String,
attrs: Map<String, String> = emptyMap(),
cb: Xml.(el: Element) -> Unit = {}
): Element {
val child = doc.createElement(name)
for ((key, value) in attrs) {
child.setAttribute(key, value)
}
return child
.also {
element.appendChild(it)
Xml(doc, it).cb(it)
}
}
fun attrs(vararg attributes: Pair<String, String>) {
for (attr in attributes) element.setAttribute(attr.first, attr.second)
}
fun attr(key: String, value: String) {
element.setAttribute(key, value)
}
fun text(txt: String) {
element.appendChild(doc.createTextNode(txt))
}
}

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>