7
src/test/java/module-info.java.backup
Normal file
7
src/test/java/module-info.java.backup
Normal file
@@ -0,0 +1,7 @@
|
||||
module net.woggioni.gbcs.test {
|
||||
requires org.junit.jupiter.api;
|
||||
requires net.woggioni.gbcs;
|
||||
requires kotlin.stdlib;
|
||||
requires java.xml;
|
||||
requires java.naming;
|
||||
}
|
227
src/test/java/net/woggioni/gbcs/utils/CertificateUtils.java
Normal file
227
src/test/java/net/woggioni/gbcs/utils/CertificateUtils.java
Normal file
@@ -0,0 +1,227 @@
|
||||
package net.woggioni.gbcs.utils;
|
||||
|
||||
import org.bouncycastle.asn1.DERSequence;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.asn1.x509.SubjectAltPublicKeyInfo;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
|
||||
public class CertificateUtils {
|
||||
|
||||
public record X509Credentials(
|
||||
KeyPair keyPair,
|
||||
X509Certificate certificate
|
||||
){ }
|
||||
public static class CertificateAuthority {
|
||||
private final PrivateKey privateKey;
|
||||
private final X509Certificate certificate;
|
||||
|
||||
public CertificateAuthority(PrivateKey privateKey, X509Certificate certificate) {
|
||||
this.privateKey = privateKey;
|
||||
this.certificate = certificate;
|
||||
}
|
||||
|
||||
public PrivateKey getPrivateKey() { return privateKey; }
|
||||
public X509Certificate getCertificate() { return certificate; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Certificate Authority (CA)
|
||||
* @param commonName The CA's common name
|
||||
* @param validityDays How long the CA should be valid for
|
||||
* @return The generated CA containing both private key and certificate
|
||||
*/
|
||||
public static X509Credentials createCertificateAuthority(String commonName, int validityDays)
|
||||
throws Exception {
|
||||
// Generate key pair
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(4096);
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
// Prepare certificate data
|
||||
X500Name issuerName = new X500Name("CN=" + commonName);
|
||||
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
|
||||
Instant now = Instant.now();
|
||||
Date startDate = Date.from(now);
|
||||
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
|
||||
|
||||
// Create certificate builder
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
issuerName,
|
||||
serialNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
issuerName,
|
||||
keyPair.getPublic()
|
||||
);
|
||||
|
||||
// Add CA extensions
|
||||
certBuilder.addExtension(
|
||||
Extension.basicConstraints,
|
||||
true,
|
||||
new BasicConstraints(true)
|
||||
);
|
||||
certBuilder.addExtension(
|
||||
Extension.keyUsage,
|
||||
true,
|
||||
new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)
|
||||
);
|
||||
|
||||
// Sign the certificate
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.build(keyPair.getPrivate());
|
||||
X509Certificate cert = new JcaX509CertificateConverter()
|
||||
.getCertificate(certBuilder.build(signer));
|
||||
|
||||
return new X509Credentials(keyPair, cert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a server certificate signed by the CA
|
||||
* @param ca The Certificate Authority to sign with
|
||||
* @param subjectName The server's common name
|
||||
* @param validityDays How long the certificate should be valid for
|
||||
* @return KeyPair containing the server's private key and certificate
|
||||
*/
|
||||
public static X509Credentials createServerCertificate(X509Credentials ca, X500Name subjectName, int validityDays)
|
||||
throws Exception {
|
||||
// Generate server key pair
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
KeyPair serverKeyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
// Prepare certificate data
|
||||
X500Name issuerName = new X500Name(ca.certificate().getSubjectX500Principal().getName());
|
||||
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
|
||||
Instant now = Instant.now();
|
||||
Date startDate = Date.from(now);
|
||||
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
|
||||
|
||||
// Create certificate builder
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
issuerName,
|
||||
serialNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
subjectName,
|
||||
serverKeyPair.getPublic()
|
||||
);
|
||||
|
||||
// Add server certificate extensions
|
||||
certBuilder.addExtension(
|
||||
Extension.basicConstraints,
|
||||
true,
|
||||
new BasicConstraints(false)
|
||||
);
|
||||
certBuilder.addExtension(
|
||||
Extension.keyUsage,
|
||||
true,
|
||||
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)
|
||||
);
|
||||
certBuilder.addExtension(
|
||||
Extension.extendedKeyUsage,
|
||||
true,
|
||||
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth})
|
||||
);
|
||||
GeneralNames subjectAltNames = GeneralNames.getInstance(
|
||||
new DERSequence(
|
||||
new GeneralName[] {
|
||||
new GeneralName(GeneralName.iPAddress, "127.0.0.1")
|
||||
}
|
||||
)
|
||||
);
|
||||
certBuilder.addExtension(
|
||||
Extension.subjectAlternativeName,
|
||||
true,
|
||||
subjectAltNames
|
||||
);
|
||||
|
||||
// Sign the certificate
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.build(ca.keyPair().getPrivate());
|
||||
X509Certificate cert = new JcaX509CertificateConverter()
|
||||
.getCertificate(certBuilder.build(signer));
|
||||
|
||||
|
||||
return new X509Credentials(serverKeyPair, cert);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client certificate signed by the CA
|
||||
* @param ca The Certificate Authority to sign with
|
||||
* @param subjectName The client's common name
|
||||
* @param validityDays How long the certificate should be valid for
|
||||
* @return KeyPair containing the client's private key and certificate
|
||||
*/
|
||||
public static X509Credentials createClientCertificate(X509Credentials ca, X500Name subjectName, int validityDays)
|
||||
throws Exception {
|
||||
// Generate client key pair
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
KeyPair clientKeyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
// Prepare certificate data
|
||||
X500Name issuerName = new X500Name(ca.certificate().getSubjectX500Principal().getName());
|
||||
BigInteger serialNumber = new BigInteger(160, new SecureRandom());
|
||||
Instant now = Instant.now();
|
||||
Date startDate = Date.from(now);
|
||||
Date endDate = Date.from(now.plus(validityDays, ChronoUnit.DAYS));
|
||||
|
||||
// Create certificate builder
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
|
||||
issuerName,
|
||||
serialNumber,
|
||||
startDate,
|
||||
endDate,
|
||||
subjectName,
|
||||
clientKeyPair.getPublic()
|
||||
);
|
||||
|
||||
// Add client certificate extensions
|
||||
certBuilder.addExtension(
|
||||
Extension.basicConstraints,
|
||||
true,
|
||||
new BasicConstraints(false)
|
||||
);
|
||||
certBuilder.addExtension(
|
||||
Extension.keyUsage,
|
||||
true,
|
||||
new KeyUsage(KeyUsage.digitalSignature)
|
||||
);
|
||||
certBuilder.addExtension(
|
||||
Extension.extendedKeyUsage,
|
||||
true,
|
||||
new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth})
|
||||
);
|
||||
|
||||
// Sign the certificate
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.build(ca.keyPair().getPrivate());
|
||||
X509Certificate cert = new JcaX509CertificateConverter()
|
||||
.getCertificate(certBuilder.build(signer));
|
||||
|
||||
return new X509Credentials(clientKeyPair, cert);
|
||||
}
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
package net.woggioni.gbcs
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertPathValidator
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.PKIXParameters
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CertificateValidationTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val keystore = KeyStore.getInstance("PKCS12")
|
||||
val keystorePath = Path.of("/home/woggioni/ssl/woggioni@f6aa5663ef26.pfx")
|
||||
Files.newInputStream(keystorePath).use {
|
||||
keystore.load(it, System.getenv("KEYPASS").toCharArray())
|
||||
}
|
||||
val pkix = CertPathValidator.getInstance("PKIX")
|
||||
val trustStore = KeyStore.getInstance("PKCS12")
|
||||
val trustStorePath = Path.of("/home/woggioni/ssl/truststore.pfx")
|
||||
|
||||
Files.newInputStream(trustStorePath).use {
|
||||
trustStore.load(it, "123456".toCharArray())
|
||||
}
|
||||
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val cert = keystore.getCertificateChain("woggioni@f6aa5663ef26").toList()
|
||||
.let(certificateFactory::generateCertPath)
|
||||
val params = PKIXParameters(trustStore)
|
||||
params.isRevocationEnabled = false
|
||||
pkix.validate(cert, params)
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
package net.woggioni.gbcs
|
||||
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ConfigurationTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
GradleBuildCacheServer.registerUrlProtocolHandler()
|
||||
val schemaUrl = this::class.java.getResource("/net/woggioni/gbcs/gbcs.xsd")
|
||||
val dbf = Xml.newDocumentBuilderFactory()
|
||||
dbf.schema = Xml.getSchema(schemaUrl)
|
||||
val db = dbf.newDocumentBuilder().apply {
|
||||
setErrorHandler(Xml.ErrorHandler(schemaUrl))
|
||||
}
|
||||
val configurationUrl = this::class.java.getResource("/net/woggioni/gbcs/gbcs-default.xml")
|
||||
val doc = configurationUrl.openStream().use(db::parse)
|
||||
Configuration.parse(doc.documentElement)
|
||||
}
|
||||
}
|
51
src/test/kotlin/net/woggioni/gbcs/test/AbstractServerTest.kt
Normal file
51
src/test/kotlin/net/woggioni/gbcs/test/AbstractServerTest.kt
Normal file
@@ -0,0 +1,51 @@
|
||||
package net.woggioni.gbcs.test
|
||||
|
||||
import net.woggioni.gbcs.GradleBuildCacheServer
|
||||
import net.woggioni.gbcs.configuration.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
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
||||
abstract class AbstractServerTest {
|
||||
|
||||
protected lateinit var cfg : Configuration
|
||||
|
||||
protected lateinit var testDir : Path
|
||||
|
||||
private var serverHandle : GradleBuildCacheServer.ServerHandle? = null
|
||||
|
||||
@BeforeAll
|
||||
fun setUp0(@TempDir tmpDir : Path) {
|
||||
this.testDir = tmpDir
|
||||
setUp()
|
||||
startServer(cfg)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDown0() {
|
||||
tearDown()
|
||||
stopServer()
|
||||
}
|
||||
|
||||
abstract fun setUp()
|
||||
|
||||
abstract fun tearDown()
|
||||
|
||||
private fun startServer(cfg : Configuration) {
|
||||
this.serverHandle = GradleBuildCacheServer(cfg).run()
|
||||
}
|
||||
|
||||
private fun stopServer() {
|
||||
this.serverHandle?.use {
|
||||
it.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
189
src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt
Normal file
189
src/test/kotlin/net/woggioni/gbcs/test/BasicAuthServerTest.kt
Normal file
@@ -0,0 +1,189 @@
|
||||
package net.woggioni.gbcs.test
|
||||
|
||||
import io.netty.handler.codec.Headers
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import net.woggioni.gbcs.AbstractNettyHttpAuthenticator.Companion.hashPassword
|
||||
import net.woggioni.gbcs.Authorizer
|
||||
import net.woggioni.gbcs.Role
|
||||
import net.woggioni.gbcs.Xml
|
||||
import net.woggioni.gbcs.configuration.Configuration
|
||||
import net.woggioni.gbcs.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.Base64
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
class BasicAuthServerTest : AbstractServerTest() {
|
||||
|
||||
companion object {
|
||||
private const val PASSWORD = "password"
|
||||
}
|
||||
|
||||
private lateinit var cacheDir : Path
|
||||
|
||||
private val random = Random(101325)
|
||||
private val keyValuePair = newEntry(random)
|
||||
|
||||
override fun setUp() {
|
||||
this.cacheDir = testDir.resolve("cache")
|
||||
val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
|
||||
val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
|
||||
cfg = Configuration.of(
|
||||
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
|
||||
host = "127.0.0.1",
|
||||
port = ServerSocket(0).localPort + 1,
|
||||
users = listOf(
|
||||
Configuration.User("user1", hashPassword(PASSWORD), setOf(readersGroup)),
|
||||
Configuration.User("user2", hashPassword(PASSWORD), setOf(writersGroup)),
|
||||
Configuration.User("user3", hashPassword(PASSWORD), setOf(readersGroup, writersGroup))
|
||||
).asSequence().map { it.name to it}.toMap(),
|
||||
groups = sequenceOf(writersGroup, readersGroup).map { it.name to it}.toMap(),
|
||||
authentication = Configuration.BasicAuthentication(),
|
||||
useVirtualThread = true,
|
||||
tls = null,
|
||||
serverPath = "/"
|
||||
)
|
||||
Xml.write(Serializer.serialize(cfg), System.out)
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
}
|
||||
|
||||
fun buildAuthorizationHeader(user : Configuration.User, password : String) : String {
|
||||
val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let{
|
||||
String(it, StandardCharsets.UTF_8)
|
||||
}
|
||||
return "Basic $b64"
|
||||
}
|
||||
|
||||
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://${cfg.host}:${cfg.port}/$key"))
|
||||
|
||||
|
||||
fun newEntry(random : Random) : Pair<String, ByteArray> {
|
||||
val key = ByteArray(0x10).let {
|
||||
random.nextBytes(it)
|
||||
Base64.getUrlEncoder().encodeToString(it)
|
||||
}
|
||||
val value = ByteArray(0x1000).also {
|
||||
random.nextBytes(it)
|
||||
}
|
||||
return key to value
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
fun putWithNoAuthorizationHeader() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
val (key, value) = keyValuePair
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
fun putAsAReaderUser() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
val (key, value) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Reader in it.roles && Role.Writer !in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
fun getAsAWriterUser() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
val (key, _) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Writer in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.GET()
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
fun putAsAWriterUser() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
val (key, value) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Writer in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
fun getAsAReaderUser() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
val (key, value) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Reader in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.GET()
|
||||
|
||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
||||
Assertions.assertArrayEquals(value, response.body())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
fun getMissingKeyAsAReaderUser() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
val (key, _) = newEntry(random)
|
||||
val user = cfg.users.values.find {
|
||||
Role.Reader in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.GET()
|
||||
|
||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
||||
}
|
||||
}
|
32
src/test/kotlin/net/woggioni/gbcs/test/ConfigurationTest.kt
Normal file
32
src/test/kotlin/net/woggioni/gbcs/test/ConfigurationTest.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
package net.woggioni.gbcs.test
|
||||
|
||||
import net.woggioni.gbcs.configuration.Configuration
|
||||
import net.woggioni.gbcs.GradleBuildCacheServer
|
||||
import net.woggioni.gbcs.Xml
|
||||
import net.woggioni.gbcs.configuration.Serializer
|
||||
import net.woggioni.gbcs.url.ClasspathUrlStreamHandlerFactoryProvider
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.net.URL
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class ConfigurationTest {
|
||||
|
||||
@Test
|
||||
fun test(@TempDir testDir : Path) {
|
||||
URL.setURLStreamHandlerFactory(ClasspathUrlStreamHandlerFactoryProvider())
|
||||
val dbf = Xml.newDocumentBuilderFactory(GradleBuildCacheServer.CONFIGURATION_SCHEMA_URL)
|
||||
val db = dbf.newDocumentBuilder()
|
||||
val configurationUrl = GradleBuildCacheServer.DEFAULT_CONFIGURATION_URL
|
||||
val doc = configurationUrl.openStream().use(db::parse)
|
||||
val cfg = Configuration.parse(doc)
|
||||
val configFile = testDir.resolve("gbcs.xml")
|
||||
Files.newOutputStream(configFile).use {
|
||||
Xml.write(Serializer.serialize(cfg), it)
|
||||
}
|
||||
val parsed = Configuration.parse(Xml.parseXml(configFile.toUri().toURL()))
|
||||
Assertions.assertEquals(cfg, parsed)
|
||||
}
|
||||
}
|
98
src/test/kotlin/net/woggioni/gbcs/test/NoAuthServerTest.kt
Normal file
98
src/test/kotlin/net/woggioni/gbcs/test/NoAuthServerTest.kt
Normal file
@@ -0,0 +1,98 @@
|
||||
package net.woggioni.gbcs.test
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import net.woggioni.gbcs.Xml
|
||||
import net.woggioni.gbcs.configuration.Configuration
|
||||
import net.woggioni.gbcs.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 kotlin.random.Random
|
||||
|
||||
|
||||
class NoAuthServerTest : AbstractServerTest() {
|
||||
|
||||
private lateinit var cacheDir : Path
|
||||
|
||||
private val random = Random(101325)
|
||||
private val keyValuePair = newEntry(random)
|
||||
|
||||
override fun setUp() {
|
||||
this.cacheDir = testDir.resolve("cache")
|
||||
cfg = Configuration.of(
|
||||
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
|
||||
host = "127.0.0.1",
|
||||
port = ServerSocket(0).localPort + 1,
|
||||
users = emptyMap(),
|
||||
groups = emptyMap(),
|
||||
authentication = null,
|
||||
useVirtualThread = true,
|
||||
tls = null,
|
||||
serverPath = "/"
|
||||
)
|
||||
Xml.write(Serializer.serialize(cfg), System.out)
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
}
|
||||
|
||||
fun newRequestBuilder(key : String) = HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://${cfg.host}:${cfg.port}/$key"))
|
||||
|
||||
fun newEntry(random : Random) : Pair<String, ByteArray> {
|
||||
val key = ByteArray(0x10).let {
|
||||
random.nextBytes(it)
|
||||
Base64.getUrlEncoder().encodeToString(it)
|
||||
}
|
||||
val value = ByteArray(0x1000).also {
|
||||
random.nextBytes(it)
|
||||
}
|
||||
return key to value
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
fun putWithNoAuthorizationHeader() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
val (key, value) = keyValuePair
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
fun getWithNoAuthorizationHeader() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
val (key, value ) = keyValuePair
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.GET()
|
||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
||||
Assertions.assertArrayEquals(value, response.body())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
fun getMissingKey() {
|
||||
val client: HttpClient = HttpClient.newHttpClient()
|
||||
|
||||
val (key, _) = newEntry(random)
|
||||
val requestBuilder = newRequestBuilder(key).GET()
|
||||
|
||||
val response: HttpResponse<ByteArray> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
||||
}
|
||||
}
|
292
src/test/kotlin/net/woggioni/gbcs/test/TlsServerTest.kt
Normal file
292
src/test/kotlin/net/woggioni/gbcs/test/TlsServerTest.kt
Normal file
@@ -0,0 +1,292 @@
|
||||
package net.woggioni.gbcs.test
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus
|
||||
import net.woggioni.gbcs.Role
|
||||
import net.woggioni.gbcs.Xml
|
||||
import net.woggioni.gbcs.configuration.Configuration
|
||||
import net.woggioni.gbcs.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 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.Base64
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
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 lateinit var cacheDir: Path
|
||||
private lateinit var serverKeyStoreFile: Path
|
||||
private lateinit var clientKeyStoreFile: Path
|
||||
private lateinit var trustStoreFile: Path
|
||||
private lateinit var serverKeyStore: KeyStore
|
||||
private lateinit var clientKeyStore: KeyStore
|
||||
private lateinit var trustStore: KeyStore
|
||||
private lateinit var ca: X509Credentials
|
||||
|
||||
private val readersGroup = Configuration.Group("readers", setOf(Role.Reader))
|
||||
private val writersGroup = Configuration.Group("writers", setOf(Role.Writer))
|
||||
private val random = Random(101325)
|
||||
private val keyValuePair = newEntry(random)
|
||||
|
||||
private val users = listOf(
|
||||
Configuration.User("user1", null, setOf(readersGroup)),
|
||||
Configuration.User("user2", null, setOf(writersGroup)),
|
||||
Configuration.User("user3", null, setOf(readersGroup, writersGroup))
|
||||
)
|
||||
|
||||
fun createKeyStoreAndTrustStore() {
|
||||
ca = CertificateUtils.createCertificateAuthority(CA_CERTIFICATE_ENTRY, 30)
|
||||
val serverCert = CertificateUtils.createServerCertificate(ca, X500Name("CN=$SERVER_CERTIFICATE_ENTRY"), 30)
|
||||
val clientCert = CertificateUtils.createClientCertificate(ca, X500Name("CN=$CLIENT_CERTIFICATE_ENTRY"), 30)
|
||||
|
||||
serverKeyStore = KeyStore.getInstance("PKCS12").apply {
|
||||
load(null, null)
|
||||
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
|
||||
setEntry(
|
||||
SERVER_CERTIFICATE_ENTRY,
|
||||
KeyStore.PrivateKeyEntry(
|
||||
serverCert.keyPair().private,
|
||||
arrayOf(serverCert.certificate(), ca.certificate)
|
||||
),
|
||||
PasswordProtection(PASSWORD.toCharArray())
|
||||
)
|
||||
}
|
||||
Files.newOutputStream(this.serverKeyStoreFile).use {
|
||||
serverKeyStore.store(it, null)
|
||||
}
|
||||
|
||||
clientKeyStore = KeyStore.getInstance("PKCS12").apply {
|
||||
load(null, null)
|
||||
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
|
||||
setEntry(
|
||||
CLIENT_CERTIFICATE_ENTRY,
|
||||
KeyStore.PrivateKeyEntry(
|
||||
clientCert.keyPair().private,
|
||||
arrayOf(clientCert.certificate(), ca.certificate)
|
||||
),
|
||||
PasswordProtection(PASSWORD.toCharArray())
|
||||
)
|
||||
}
|
||||
Files.newOutputStream(this.clientKeyStoreFile).use {
|
||||
clientKeyStore.store(it, null)
|
||||
}
|
||||
|
||||
trustStore = KeyStore.getInstance("PKCS12").apply {
|
||||
load(null, null)
|
||||
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
|
||||
}
|
||||
Files.newOutputStream(this.trustStoreFile).use {
|
||||
trustStore.store(it, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun getClientKeyStore(ca : X509Credentials, subject: X500Name) = KeyStore.getInstance("PKCS12").apply {
|
||||
val clientCert = CertificateUtils.createClientCertificate(ca, subject, 30)
|
||||
|
||||
load(null, null)
|
||||
setEntry(CA_CERTIFICATE_ENTRY, KeyStore.TrustedCertificateEntry(ca.certificate), PasswordProtection(null))
|
||||
setEntry(
|
||||
CLIENT_CERTIFICATE_ENTRY,
|
||||
KeyStore.PrivateKeyEntry(clientCert.keyPair().private, arrayOf(clientCert.certificate(), ca.certificate)),
|
||||
PasswordProtection(PASSWORD.toCharArray())
|
||||
)
|
||||
}
|
||||
|
||||
fun getHttpClient(clientKeyStore : KeyStore?): HttpClient {
|
||||
val kmf = clientKeyStore?.let {
|
||||
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
|
||||
init(it, PASSWORD.toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set up trust manager factory with the truststore
|
||||
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
tmf.init(trustStore)
|
||||
|
||||
// Create SSL context with the key and trust managers
|
||||
val sslContext = SSLContext.getInstance("TLS").apply {
|
||||
init(kmf?.keyManagers ?: emptyArray(), tmf.trustManagers, null)
|
||||
}
|
||||
return HttpClient.newBuilder().sslContext(sslContext).build()
|
||||
}
|
||||
|
||||
override fun 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()
|
||||
cfg = Configuration.of(
|
||||
cache = Configuration.FileSystemCache(this.cacheDir, maxAge = Duration.ofSeconds(3600 * 24)),
|
||||
host = "127.0.0.1",
|
||||
port = ServerSocket(0).localPort + 1,
|
||||
users = users.asSequence().map { it.name to it }.toMap(),
|
||||
groups = sequenceOf(writersGroup, readersGroup).map { it.name to it }.toMap(),
|
||||
authentication = Configuration.ClientCertificateAuthentication(
|
||||
userExtractor = Configuration.TlsCertificateExtractor("CN", "(.*)"),
|
||||
groupExtractor = null
|
||||
),
|
||||
useVirtualThread = true,
|
||||
tls = Configuration.Tls(
|
||||
Configuration.KeyStore(this.serverKeyStoreFile, null, SERVER_CERTIFICATE_ENTRY, PASSWORD),
|
||||
Configuration.TrustStore(this.trustStoreFile, null, false),
|
||||
true
|
||||
),
|
||||
serverPath = "/"
|
||||
)
|
||||
Xml.write(Serializer.serialize(cfg), System.out)
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
}
|
||||
|
||||
fun newRequestBuilder(key: String) = HttpRequest.newBuilder()
|
||||
.uri(URI.create("https://${cfg.host}:${cfg.port}/$key"))
|
||||
|
||||
fun buildAuthorizationHeader(user: Configuration.User, password: String): String {
|
||||
val b64 = Base64.getEncoder().encode("${user.name}:${password}".toByteArray(Charsets.UTF_8)).let {
|
||||
String(it, StandardCharsets.UTF_8)
|
||||
}
|
||||
return "Basic $b64"
|
||||
}
|
||||
|
||||
fun newEntry(random: Random): Pair<String, ByteArray> {
|
||||
val key = ByteArray(0x10).let {
|
||||
random.nextBytes(it)
|
||||
Base64.getUrlEncoder().encodeToString(it)
|
||||
}
|
||||
val value = ByteArray(0x1000).also {
|
||||
random.nextBytes(it)
|
||||
}
|
||||
return key to value
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
fun putWithNoClientCertificate() {
|
||||
val client: HttpClient = getHttpClient(null)
|
||||
val (key, value) = keyValuePair
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.UNAUTHORIZED.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
fun putAsAReaderUser() {
|
||||
val (key, value) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Reader in it.roles && Role.Writer !in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
fun getAsAWriterUser() {
|
||||
|
||||
val (key, _) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Writer in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.GET()
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.FORBIDDEN.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
fun putAsAWriterUser() {
|
||||
|
||||
val (key, value) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Writer in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.PUT(HttpRequest.BodyPublishers.ofByteArray(value))
|
||||
|
||||
val response: HttpResponse<String> = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
|
||||
Assertions.assertEquals(HttpResponseStatus.CREATED.code(), response.statusCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
fun getAsAReaderUser() {
|
||||
val (key, value) = keyValuePair
|
||||
val user = cfg.users.values.find {
|
||||
Role.Reader in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.GET()
|
||||
|
||||
val response: HttpResponse<ByteArray> =
|
||||
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||
Assertions.assertEquals(HttpResponseStatus.OK.code(), response.statusCode())
|
||||
Assertions.assertArrayEquals(value, response.body())
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
fun getMissingKeyAsAReaderUser() {
|
||||
val (key, _) = newEntry(random)
|
||||
val user = cfg.users.values.find {
|
||||
Role.Reader in it.roles
|
||||
} ?: throw RuntimeException("Reader user not found")
|
||||
val client: HttpClient = getHttpClient(getClientKeyStore(ca, X500Name("CN=${user.name}")))
|
||||
|
||||
val requestBuilder = newRequestBuilder(key)
|
||||
.header("Authorization", buildAuthorizationHeader(user, PASSWORD))
|
||||
.GET()
|
||||
|
||||
val response: HttpResponse<ByteArray> =
|
||||
client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray())
|
||||
Assertions.assertEquals(HttpResponseStatus.NOT_FOUND.code(), response.statusCode())
|
||||
}
|
||||
}
|
19
src/test/kotlin/net/woggioni/gbcs/test/X500NameTest.kt
Normal file
19
src/test/kotlin/net/woggioni/gbcs/test/X500NameTest.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package net.woggioni.gbcs.test
|
||||
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import javax.naming.ldap.LdapName
|
||||
|
||||
class X500NameTest {
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
val 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"
|
||||
val ldapName = LdapName(name)
|
||||
val value = ldapName.rdns.asSequence().find {
|
||||
it.type == "CN"
|
||||
}!!.value
|
||||
Assertions.assertEquals("woggioni@f6aa5663ef26", value)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user