code simplification

This commit is contained in:
2025-01-09 00:05:00 +08:00
parent 0fdb37fb54
commit d5a2c4a591
24 changed files with 11 additions and 1853 deletions

View File

@@ -103,50 +103,6 @@ tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) {
options.javaModuleMainClass = mainClassName
}
//tasks.named(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME, JavaCompile) {
// options.compilerArgs << '--patch-module' << 'net.woggioni.gbcs.test=' + project.sourceSets.test.output.asPath
// classpath = configurations.testCompileClasspath + files(tasks.jar.archiveFile)
// modularity.inferModulePath = true
// javaModuleDetector
//}
//tasks.named(JavaPlugin.TEST_TASK_NAME, JavaForkOptions) {
// classpath = configurations.testRuntimeClasspath + project.files(tasks.jar.archiveFile) + project.sourceSets.test.output
// jvmArgumentProviders << new CommandLineArgumentProvider() {
// @CompileClasspath
// def kotlinClassesMain = kotlin.sourceSets.main.collect { it.kotlin.classesDirectory }
//
// @CompileClasspath
// def kotlinClassesTest = kotlin.sourceSets.main.collect { it.kotlin.classesDirectory }
// @Override
// Iterable<String> asArguments() {
// return [
// "--patch-module",
// 'net.woggioni.gbcs=' + kotlinClassesMain.collect { it.get().asFile.absolutePath },
// "--patch-module",
// 'net.woggioni.gbcs.test=' + project.sourceSets.test.output.asPath,
// ]
// }
// }
//}
//configurations {
// integrationTestImplementation {
// attributes {
// attribute(LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements.class, JAR))
// }
// }
//
// integrationTestCompileClasspath {
// attributes {
// attribute(LIBRARY_ELEMENTS_ATTRIBUTE, project.objects.named(LibraryElements.class, JAR))
// }
// }
//}
//
envelopeJar {
mainModule = 'net.woggioni.gbcs'
mainClass = mainClassName
@@ -154,37 +110,6 @@ envelopeJar {
extraClasspath = ["plugins"]
}
//
//testing {
// suites {
// test {
// useJUnitJupiter(catalog.versions.junit.jupiter.get())
// }
//
// integrationTest(JvmTestSuite) {
// dependencies {
// implementation project()
// implementation catalog.bcprov.jdk18on
// implementation catalog.bcpkix.jdk18on
// annotationProcessor catalog.lombok
// compileOnly catalog.lombok
// implementation project('gbcs-base')
// implementation project('gbcs-api')
//
// runtimeOnly project("gbcs-memcached")
// }
//
// targets {
// all {
// testTask.configure {
// shouldRunAfter(test)
// }
// }
// }
// }
// }
//}
dependencies {
implementation catalog.jwo
implementation catalog.slf4j.api
@@ -235,19 +160,3 @@ publishing {
}
}
//tasks.named('check') {
// dependsOn(testing.suites.integrationTest)
//}
//
//tasks.named("integrationTest", JavaForkOptions) {
// jvmArgumentProviders << new CommandLineArgumentProvider() {
// @Override
// Iterable<String> asArguments() {
// return [
// "--patch-module",
// 'net.woggioni.gbcs.test=' + project.sourceSets.integrationTest.output.asPath,
// ]
// }
// }
//}

View File

@@ -2,8 +2,6 @@ package net.woggioni.gbcs.api;
import lombok.Value;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.nio.file.Path;
import java.security.cert.X509Certificate;
@@ -106,12 +104,6 @@ public class Configuration {
String getTypeName();
}
// @Value
// public static class FileSystemCache implements Cache {
// Path root;
// Duration maxAge;
// }
public static Configuration of(
String host,
int port,

View File

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

View File

@@ -128,8 +128,6 @@ class Xml(val doc: Document, val element: Element) {
fun getSchema(schema: URL): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, false)
// disableProperty(sf, ACCESS_EXTERNAL_SCHEMA)
// disableProperty(sf, ACCESS_EXTERNAL_DTD)
sf.errorHandler = ErrorHandler(schema)
return sf.newSchema(schema)
}
@@ -137,15 +135,12 @@ class Xml(val doc: Document, val element: Element) {
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, false)
// disableProperty(dbf, ACCESS_EXTERNAL_SCHEMA)
dbf.setAttribute(ACCESS_EXTERNAL_SCHEMA, "all")
disableProperty(dbf, ACCESS_EXTERNAL_DTD)
dbf.isExpandEntityReferences = true
@@ -175,32 +170,10 @@ class Xml(val doc: Document, val element: Element) {
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")
@@ -230,7 +203,6 @@ class Xml(val doc: Document, val element: Element) {
removeChild(firstChild ?: break)
}
}
}
fun node(
@@ -250,15 +222,6 @@ class Xml(val doc: Document, val element: Element) {
}
}
// fun attrs(vararg attributes: Pair<String, String>) {
// for (attr in attributes) element.setAttribute(attr.first, attr.second)
// }
//
// fun attrs(vararg attributes: Pair<Pair<String?, String>, String>) {
// for (attr in attributes) element.setAttributeNS(attr.first.first, attr.first.second, attr.second)
// }
fun attr(key: String, value: String, namespaceURI : String? = null) {
element.setAttributeNS(namespaceURI, key, value)
}

View File

@@ -1,15 +0,0 @@
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

@@ -1,51 +0,0 @@
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

@@ -1,229 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -1,128 +0,0 @@
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

@@ -1,357 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,227 +0,0 @@
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

@@ -1,13 +0,0 @@
<?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

@@ -3,12 +3,6 @@ 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;
@@ -25,12 +19,8 @@ open module net.woggioni.gbcs {
requires net.woggioni.gbcs.base;
requires net.woggioni.gbcs.api;
// 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

@@ -61,14 +61,6 @@ abstract class AbstractNettyHttpAuthenticator(private val authorizer : Authorize
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
}
// fun decodePasswordHash(passwordHash : String) : Pair<String, String> {
// return passwordHash.indexOf(':')
// .takeIf { it > 0 }
// ?.let { sep ->
// passwordHash.substring(0, sep) to passwordHash.substring(sep)
// } ?: throw IllegalArgumentException("Failed to decode password hash")
// }
fun decodePasswordHash(passwordHash : String) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(passwordHash)
val hash = ByteArray(KEY_LENGTH / 8)

View File

@@ -46,9 +46,8 @@ 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.Xml
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
@@ -76,7 +75,6 @@ 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) {
@@ -540,7 +538,6 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
}
fun loadConfiguration(args: Array<String>): Configuration {
// registerUrlProtocolHandler()
// Thread.currentThread().contextClassLoader = GradleBuildCacheServer::class.java.classLoader
val app = Application.builder("gbcs")
.configurationDirectoryEnvVar("GBCS_CONFIGURATION_DIR")

View File

@@ -1,309 +0,0 @@
package net.woggioni.gbcs.configuration
import net.woggioni.gbcs.api.Role
import java.nio.file.Path
import java.security.cert.X509Certificate
import java.time.Duration
@ConsistentCopyVisibility
data class Configuration private constructor(
val host: String,
val port: Int,
val serverPath: String?,
val users: Map<String, User>,
val groups: Map<String, Group>,
val cache: Cache,
val authentication : Authentication?,
val tls: Tls?,
val useVirtualThread: Boolean
) {
data class Group(val name: String, val roles: Set<Role>) {
override fun hashCode(): Int {
return name.hashCode()
}
}
data class User(val name: String, val password: String?, val groups: Set<Group>) {
override fun hashCode(): Int {
return name.hashCode()
}
val roles : Set<Role>
get() = groups.asSequence().flatMap { it.roles }.toSet()
}
fun interface UserExtractor {
fun extract(cert :X509Certificate) : User
}
fun interface GroupExtractor {
fun extract(cert :X509Certificate) : Group
}
data class Tls(
val keyStore: KeyStore?,
val trustStore: TrustStore?,
val verifyClients: Boolean,
)
data class KeyStore(
val file: Path,
val password: String?,
val keyAlias: String,
val keyPassword: String?
)
data class TrustStore(
val file: Path,
val password: String?,
val checkCertificateStatus: Boolean
)
data class TlsCertificateExtractor(val rdnType : String, val pattern : String)
interface Authentication
class BasicAuthentication : Authentication
data class ClientCertificateAuthentication(
val userExtractor: TlsCertificateExtractor?,
val groupExtractor: TlsCertificateExtractor?) : Authentication
interface Cache
data class FileSystemCache(val root: Path, val maxAge: Duration) : Cache
companion object {
fun of(
host: String,
port: Int,
serverPath: String?,
users: Map<String, User>,
groups: Map<String, Group>,
cache: Cache,
authentication : Authentication?,
tls: Tls?,
useVirtualThread: Boolean
) = Configuration(
host,
port,
serverPath?.takeIf { it.isNotEmpty() && it != "/" },
users,
groups,
cache,
authentication,
tls,
useVirtualThread
)
// 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

@@ -16,6 +16,7 @@ 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.lang.IllegalArgumentException
import java.nio.file.Paths
object Parser {
@@ -57,45 +58,12 @@ object Parser {
}
"cache" -> {
// val type = child.getAttribute("xs:type").split(":")
// val namespaceURI = child.lookupNamespaceURI(type[0])
// val typeName = type[1]
cache = (child as? TypeInfo)?.let { tf ->
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)
// }
// }
// }
?: throw IllegalArgumentException("Cache provider for namespace '$typeNamespace' not found")
}.deserialize(child)
}
"authentication" -> {

View File

@@ -4,7 +4,6 @@ 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.ClassOrderer
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestMethodOrder
@@ -14,7 +13,7 @@ import java.nio.file.Path
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
abstract class AbstractServerTestKt {
abstract class AbstractServerTest {
protected lateinit var cfg : Configuration

View File

@@ -23,7 +23,7 @@ import java.util.zip.Deflater
import kotlin.random.Random
class BasicAuthServerTestKt : AbstractServerTestKt() {
class BasicAuthServerTest : AbstractServerTest() {
companion object {
private const val PASSWORD = "password"

View File

@@ -9,17 +9,10 @@ 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.net.URL
import java.nio.file.Files
import java.nio.file.Path
class ConfigurationTestKt {
// companion object {
// init {
// URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
// }
// }
class ConfigurationTest {
@ValueSource(
strings = [

View File

@@ -20,7 +20,7 @@ import java.util.zip.Deflater
import kotlin.random.Random
class NoAuthServerTestKt : AbstractServerTestKt() {
class NoAuthServerTest : AbstractServerTest() {
private lateinit var cacheDir : Path

View File

@@ -31,16 +31,13 @@ import javax.net.ssl.TrustManagerFactory
import kotlin.random.Random
class TlsServerTestKt : AbstractServerTestKt() {
class TlsServerTest : AbstractServerTest() {
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

View File

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