code simplification
This commit is contained in:
91
build.gradle
91
build.gradle
@@ -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,
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//}
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
|
@@ -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)
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
||||
// }
|
||||
}
|
||||
}
|
@@ -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" -> {
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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 = [
|
||||
|
@@ -20,7 +20,7 @@ import java.util.zip.Deflater
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
class NoAuthServerTestKt : AbstractServerTestKt() {
|
||||
class NoAuthServerTest : AbstractServerTest() {
|
||||
|
||||
private lateinit var cacheDir : Path
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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() {
|
||||
|
Reference in New Issue
Block a user