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