diff --git a/plugins/maven/testFramework/BUILD.bazel b/plugins/maven/testFramework/BUILD.bazel
index de1bc9268246..ceef32429cf2 100644
--- a/plugins/maven/testFramework/BUILD.bazel
+++ b/plugins/maven/testFramework/BUILD.bazel
@@ -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
\ No newline at end of file
diff --git a/plugins/maven/testFramework/intellij.maven.testFramework.iml b/plugins/maven/testFramework/intellij.maven.testFramework.iml
index 49fc21dbc857..fa7ba913a699 100644
--- a/plugins/maven/testFramework/intellij.maven.testFramework.iml
+++ b/plugins/maven/testFramework/intellij.maven.testFramework.iml
@@ -49,5 +49,6 @@
+
\ No newline at end of file
diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/AbstractMavenRepositoryServerFixture.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/AbstractMavenRepositoryServerFixture.kt
new file mode 100644
index 000000000000..553b32a1f68d
--- /dev/null
+++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/AbstractMavenRepositoryServerFixture.kt
@@ -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
", *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
+}
\ No newline at end of file
diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenCertificateFixture.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenCertificateFixture.kt
new file mode 100644
index 000000000000..94970f8291f7
--- /dev/null
+++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenCertificateFixture.kt
@@ -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 {
+ 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 {
+ 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() {
+ }
+}
\ No newline at end of file
diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpRepositoryServerFixture.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpRepositoryServerFixture.kt
index 14cc7e1dec3a..10a87874b8b7 100644
--- a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpRepositoryServerFixture.kt
+++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpRepositoryServerFixture.kt
@@ -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
", *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
}
}
\ No newline at end of file
diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpsRepositoryServerFixture.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpsRepositoryServerFixture.kt
new file mode 100644
index 000000000000..bf5d1537ed4a
--- /dev/null
+++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/utils/MavenHttpsRepositoryServerFixture.kt
@@ -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())
+ }
+ }
+}
\ No newline at end of file