[maven] [IDEA-368366] create SSL infrastructure for tests

GitOrigin-RevId: a3346d15cce231be4c37c8e8f70375153923859a
This commit is contained in:
Alexander Bubenchikov
2025-08-06 09:22:56 +00:00
committed by intellij-monorepo-bot
parent 98b827095f
commit ebfefbf34b
6 changed files with 283 additions and 80 deletions
+1
View File
@@ -32,6 +32,7 @@ jvm_library(
"//platform/backend/observation",
"//platform/backend/workspace",
"//platform/workspace/storage",
"@lib//:bouncy-castle-provider",
]
)
### auto-generated section `build intellij.maven.testFramework` end
@@ -49,5 +49,6 @@
<orderEntry type="module" module-name="intellij.platform.backend.observation" />
<orderEntry type="module" module-name="intellij.platform.backend.workspace" />
<orderEntry type="module" module-name="intellij.platform.workspace.storage" />
<orderEntry type="library" name="bouncy-castle-provider" level="project" />
</component>
</module>
@@ -0,0 +1,85 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.maven.testFramework.utils
import com.intellij.openapi.util.io.StreamUtil
import com.intellij.testFramework.fixtures.IdeaTestFixture
import com.sun.net.httpserver.Authenticator
import com.sun.net.httpserver.BasicAuthenticator
import com.sun.net.httpserver.HttpServer
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.stream.Collectors
import kotlin.use
abstract class AbstractMavenRepositoryServerFixture : IdeaTestFixture {
protected lateinit var myServer: HttpServer
private set
abstract fun url(): String
override fun setUp() {
myServer = startServer()
}
override fun tearDown() {
if (this::myServer.isInitialized) {
myServer.stop(10)
}
}
fun startRepositoryFor(repo: Path, expectedUsername: String, expectedPassword: String) {
startRepositoryFor(repo, "/", expectedUsername, expectedPassword)
}
fun startRepositoryFor(repo: Path, contextPath: String = "/", expectedUsername: String? = null, expectedPassword: String? = null) {
val authenticator: Authenticator? = if (expectedUsername == null) null
else object : BasicAuthenticator("/") {
override fun checkCredentials(username: String?, password: String?): Boolean {
return expectedUsername == username && expectedPassword == password && expectedPassword != null
}
}
setupRemoteRepositoryServerReadFiles(repo, contextPath, authenticator)
}
fun startRepositoryFor(repo: String) {
startRepositoryFor(Path.of(repo), "/", null, null)
}
private fun setupRemoteRepositoryServerReadFiles(repo: Path, contextPath: String, authenticator: Authenticator?) {
val httpContext = myServer.createContext(contextPath) { exchange ->
val path = exchange.requestURI.path
//MavenLog.LOG.warn("Got request for $path")
val file = repo.resolve(path.removePrefix("/")).normalize()
if (Files.isDirectory(file)) {
exchange.responseHeaders.add("Content-Type", "text/html")
exchange.sendResponseHeaders(200, 0)
val listing = Files.list(file).use { stream ->
stream.map { it.fileName.toString() }
.collect(Collectors.toList())
.toTypedArray()
}
val list = java.lang.String.join(",\n<br/>", *listing)
exchange.responseBody.write(list.toByteArray(StandardCharsets.UTF_8))
}
else if (Files.isRegularFile(file)) {
exchange.responseHeaders.add("Content-Type", "application/octet-stream")
exchange.sendResponseHeaders(200, 0)
Files.newInputStream(file, StandardOpenOption.READ).use {
StreamUtil.copy(it, exchange.responseBody)
}
}
else {
exchange.sendResponseHeaders(404, -1)
}
exchange.close()
//MavenLog.LOG.warn("Sent response for $path")
}
httpContext.authenticator = authenticator
}
protected abstract fun startServer(): HttpServer
}
@@ -0,0 +1,109 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.maven.testFramework.utils
import com.intellij.testFramework.fixtures.IdeaTestFixture
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.BasicConstraints
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.math.BigInteger
import java.nio.file.Path
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.util.*
class MavenCertificateFixture() : IdeaTestFixture {
private val rootCaKeyPair: KeyPair = generateKeyPair()
private val rootCaCert: X509Certificate = generateRootCaCertificate()
override fun setUp() {
}
private fun generateKeyPair(): KeyPair {
val keyGen = KeyPairGenerator.getInstance("RSA")
keyGen.initialize(2048)
return keyGen.generateKeyPair()
}
private fun generateRootCaCertificate(): X509Certificate {
val issuer = X500Name("CN=Test Root CA")
val serial = BigInteger.valueOf(System.currentTimeMillis())
val now = Date()
val expiry = Date(now.time + 365L * 24 * 60 * 60 * 1000)
val certBuilder = JcaX509v3CertificateBuilder(
issuer, serial, now, expiry, issuer, rootCaKeyPair.public
)
certBuilder.addExtension(Extension.basicConstraints, true, BasicConstraints(true))
val signer = JcaContentSignerBuilder("SHA256withRSA").build(rootCaKeyPair.private)
return JcaX509CertificateConverter().getCertificate(certBuilder.build(signer))
}
fun createServerCertificate(hostname: String): Pair<X509Certificate, PrivateKey> {
val keyPair = generateKeyPair()
val subject = X500Name("CN=$hostname")
val serial = BigInteger.valueOf(System.currentTimeMillis())
val now = Date()
val expiry = Date(now.time + 365L * 24 * 60 * 60 * 1000)
val certBuilder = JcaX509v3CertificateBuilder(
X500Name(rootCaCert.subjectX500Principal.name),
serial, now, expiry, subject, keyPair.public
)
val signer = JcaContentSignerBuilder("SHA256withRSA").build(rootCaKeyPair.private)
return JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)) to keyPair.private
}
fun createClientCertificate(cn: String): Pair<X509Certificate, PrivateKey> {
val keyPair = generateKeyPair()
val subject = X500Name("CN=$cn")
val serial = BigInteger.valueOf(System.currentTimeMillis())
val now = Date()
val expiry = Date(now.time + 365L * 24 * 60 * 60 * 1000)
val certBuilder = JcaX509v3CertificateBuilder(
X500Name(rootCaCert.subjectX500Principal.name),
serial, now, expiry, subject, keyPair.public
)
val signer = JcaContentSignerBuilder("SHA256withRSA").build(rootCaKeyPair.private)
return JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)) to keyPair.private
}
fun checkClientCertificate(cert: X509Certificate) {
try {
cert.verify(rootCaCert.publicKey)
val issuerDN = cert.issuerX500Principal
val rootDN = rootCaCert.subjectX500Principal
if (issuerDN != rootDN) {
throw SecurityException("Certificate was not issued by the root CA")
}
}
catch (e: Exception) {
throw SecurityException("Invalid client certificate", e)
}
}
fun saveCertificates(cert: X509Certificate, store: Path, password: String, type: String = "pkcs12") {
val keyStore = KeyStore.getInstance(type)
keyStore.load(null, null)
keyStore.setCertificateEntry("cert-${UUID.randomUUID()}", cert)
saveKeyStore(keyStore, store, password)
}
private fun saveKeyStore(keyStore: KeyStore, storePath: Path, password: String) {
storePath.toFile().outputStream().use { os ->
keyStore.store(os, password.toCharArray())
}
}
override fun tearDown() {
}
}
@@ -1,94 +1,23 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.maven.testFramework.utils
import com.intellij.ReviseWhenPortedToJDK
import com.intellij.openapi.util.io.StreamUtil
import com.intellij.testFramework.fixtures.IdeaTestFixture
import com.intellij.util.concurrency.AppExecutorUtil
import com.sun.net.httpserver.Authenticator
import com.sun.net.httpserver.BasicAuthenticator
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.stream.Collectors
private const val LOCALHOST = "127.0.0.1"
@ReviseWhenPortedToJDK("18") //replace with SimpleFileServers
class MavenHttpRepositoryServerFixture : IdeaTestFixture {
private lateinit var myServer: HttpServer
fun url(): String {
if (!this::myServer.isInitialized) {
throw IllegalStateException("Url is not ready yet, call setUp first")
}
class MavenHttpRepositoryServerFixture : AbstractMavenRepositoryServerFixture() {
override fun url(): String {
return "http://" + LOCALHOST + ":" + myServer.address.port
}
override fun setUp() {
myServer = HttpServer.create()
myServer.setExecutor(AppExecutorUtil.getAppExecutorService())
myServer.bind(InetSocketAddress(LOCALHOST, 0), 5)
myServer.start()
}
override fun tearDown() {
if (this::myServer.isInitialized) {
myServer.stop(10)
}
}
fun startRepositoryFor(repo: Path, expectedUsername: String, expectedPassword: String) {
startRepositoryFor(repo, "/", expectedUsername, expectedPassword)
}
fun startRepositoryFor(repo: Path, contextPath: String = "/", expectedUsername: String? = null, expectedPassword: String? = null) {
val authenticator: Authenticator? = if (expectedUsername == null) null
else object : BasicAuthenticator("/") {
override fun checkCredentials(username: String?, password: String?): Boolean {
return expectedUsername == username && expectedPassword == password && expectedPassword != null
}
}
setupRemoteRepositoryServerReadFiles(repo, contextPath, authenticator)
}
fun startRepositoryFor(repo: String) {
startRepositoryFor(Path.of(repo), "/", null, null)
}
private fun setupRemoteRepositoryServerReadFiles(repo: Path, contextPath: String, authenticator: Authenticator?) {
val httpContext = myServer.createContext(contextPath) { exchange ->
val path = exchange.requestURI.path
//MavenLog.LOG.warn("Got request for $path")
val file = repo.resolve(path.removePrefix("/")).normalize()
if (Files.isDirectory(file)) {
exchange.responseHeaders.add("Content-Type", "text/html")
exchange.sendResponseHeaders(200, 0)
val listing = Files.list(file).use { stream ->
stream.map { it.fileName.toString() }
.collect(Collectors.toList())
.toTypedArray()
}
val list = java.lang.String.join(",\n<br/>", *listing)
exchange.responseBody.write(list.toByteArray(StandardCharsets.UTF_8))
}
else if (Files.isRegularFile(file)) {
exchange.responseHeaders.add("Content-Type", "application/octet-stream")
exchange.sendResponseHeaders(200, 0)
Files.newInputStream(file, StandardOpenOption.READ).use {
StreamUtil.copy(it, exchange.responseBody)
}
}
else {
exchange.sendResponseHeaders(404, -1)
}
exchange.close()
//MavenLog.LOG.warn("Sent response for $path")
}
httpContext.authenticator = authenticator
override fun startServer(): HttpServer {
val server = HttpServer.create()
server.setExecutor(AppExecutorUtil.getAppExecutorService())
server.bind(InetSocketAddress(LOCALHOST, 0), 5)
server.start()
return server
}
}
@@ -0,0 +1,78 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.maven.testFramework.utils
import com.intellij.util.concurrency.AppExecutorUtil
import com.sun.net.httpserver.HttpsConfigurator
import com.sun.net.httpserver.HttpsParameters
import com.sun.net.httpserver.HttpsServer
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.io.File
import java.net.InetSocketAddress
import java.security.KeyStore
import java.security.PrivateKey
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import kotlin.time.Duration.Companion.seconds
private const val LOCALHOST = "127.0.0.1"
private const val SERVER_KS_PASSWORD = "password"
class MavenHttpsRepositoryServerFixture(
val myServerCertificate: X509Certificate,
val sslHostname: String,
val myPrivateKey: PrivateKey,
) : AbstractMavenRepositoryServerFixture() {
override fun url(): String {
return "https://$LOCALHOST:${myServer.address.port}"
}
override fun startServer(): HttpsServer {
val server = HttpsServer.create()
server.bind(InetSocketAddress(LOCALHOST, 0), 0)
val sslContext = createSSLContext()
server.httpsConfigurator = object : HttpsConfigurator(sslContext) {
override fun configure(params: HttpsParameters) {
val engine = sslContext.createSSLEngine()
params.needClientAuth = false
params.cipherSuites = engine.enabledCipherSuites
params.protocols = engine.enabledProtocols
params.setSSLParameters(sslContext.defaultSSLParameters)
}
}
server.executor = AppExecutorUtil.getAppExecutorService()
server.start()
return server
}
private fun createSSLContext(): SSLContext {
// Initialize an in-memory KeyStore
val keyStore = KeyStore.getInstance("JKS").apply {
load(null, null)
setKeyEntry(
sslHostname,
myPrivateKey,
SERVER_KS_PASSWORD.toCharArray(),
arrayOf(myServerCertificate)
)
}
// KeyManagerFactory for server certificate
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(keyStore, SERVER_KS_PASSWORD.toCharArray())
// TrustManagerFactory to trust our own cert
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
tmf.init(keyStore)
return SSLContext.getInstance("TLS").apply {
init(kmf.keyManagers, tmf.trustManagers, SecureRandom())
}
}
}