-
Notifications
You must be signed in to change notification settings - Fork 273
feat(lsp): respect IDE user proxy settings / forward trust store #5553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
82f7313
cf0ba75
b8dcb4d
0f73bf6
3328c9f
8b08e5c
92e93b4
548fa25
ed5f0c8
57ecb8b
fb46994
c243301
37fd987
a7e7872
2276592
ecf1779
190427f
1644d96
9fd2e21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
| import com.intellij.openapi.util.Key | ||
| import com.intellij.openapi.util.SystemInfo | ||
| import com.intellij.util.io.await | ||
| import com.intellij.util.net.JdkProxyProvider | ||
| import com.intellij.util.net.ProxyAuthentication | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Deferred | ||
| import kotlinx.coroutines.TimeoutCancellationException | ||
|
|
@@ -29,6 +31,7 @@ | |
| import kotlinx.coroutines.sync.Mutex | ||
| import kotlinx.coroutines.sync.withLock | ||
| import kotlinx.coroutines.withTimeout | ||
| import org.apache.http.client.utils.URIBuilder | ||
| import org.eclipse.lsp4j.ClientCapabilities | ||
| import org.eclipse.lsp4j.ClientInfo | ||
| import org.eclipse.lsp4j.DidChangeConfigurationParams | ||
|
|
@@ -45,6 +48,7 @@ | |
| import software.aws.toolkits.core.utils.getLogger | ||
| import software.aws.toolkits.core.utils.info | ||
| import software.aws.toolkits.core.utils.warn | ||
| import software.aws.toolkits.core.utils.writeText | ||
| import software.aws.toolkits.jetbrains.isDeveloperMode | ||
| import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager | ||
| import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCredentialsService | ||
|
|
@@ -54,6 +58,7 @@ | |
| import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler | ||
| import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders | ||
| import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler | ||
| import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints | ||
| import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata | ||
| import software.aws.toolkits.jetbrains.settings.LspSettings | ||
| import java.io.IOException | ||
|
|
@@ -62,7 +67,11 @@ | |
| import java.io.PipedOutputStream | ||
| import java.io.PrintWriter | ||
| import java.io.StringWriter | ||
| import java.net.Proxy | ||
| import java.net.URI | ||
| import java.nio.charset.StandardCharsets | ||
| import java.nio.file.Files | ||
| import java.util.Base64 | ||
| import java.util.concurrent.Future | ||
| import kotlin.time.Duration.Companion.seconds | ||
|
|
||
|
|
@@ -250,13 +259,49 @@ | |
| init { | ||
| // will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress | ||
| val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath() | ||
|
|
||
| // more slowness | ||
|
||
| // make assumption that all requests will resolve to the same CA | ||
| // also terrible assumption that default endpoint is reachable | ||
| val qUri = URI(QEndpoints.Q_DEFAULT_SERVICE_CONFIG.ENDPOINT) | ||
| val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) | ||
| val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply { | ||
| writeText( | ||
| TrustChainUtil.certsToPem(rtsTrustChain) | ||
| ) | ||
| } | ||
|
|
||
| val node = if (SystemInfo.isWindows) "node.exe" else "node" | ||
| val cmd = GeneralCommandLine( | ||
| artifact.resolve(node).toString(), | ||
| LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(), | ||
| "--stdio", | ||
| "--set-credentials-encryption-key", | ||
| ).withEnvironment( | ||
| buildMap { | ||
| put("NODE_EXTRA_CA_CERTS", extraCaCerts.toAbsolutePath().toString()) | ||
|
|
||
| val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri) | ||
| // log if only socks proxy available | ||
| .firstOrNull { it.type() == Proxy.Type.HTTP } | ||
|
|
||
| if (proxy != null) { | ||
| val address = proxy.address() | ||
| if (address is java.net.InetSocketAddress) { | ||
| put( | ||
| "HTTPS_PROXY", | ||
| URIBuilder("http://${address.hostName}:${address.port}").apply { | ||
| val login = ProxyAuthentication.getInstance().getKnownAuthentication(address.hostName, address.port) | ||
| if (login != null) { | ||
| setUserInfo(login.userName, login.getPasswordAsString()) | ||
| } | ||
| }.build().toASCIIString() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| ) | ||
| .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) | ||
|
|
||
| launcherHandler = KillableColoredProcessHandler.Silent(cmd) | ||
| val inputWrapper = LSPProcessListener() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp | ||
|
|
||
| import com.intellij.util.io.DigestUtil | ||
| import com.intellij.util.net.JdkProxyProvider | ||
| import com.intellij.util.net.ssl.CertificateManager | ||
| import org.apache.http.client.methods.RequestBuilder | ||
| import org.apache.http.conn.ssl.DefaultHostnameVerifier | ||
| import org.apache.http.impl.client.HttpClientBuilder | ||
| import org.apache.http.impl.client.SystemDefaultCredentialsProvider | ||
| import org.apache.http.impl.conn.SystemDefaultRoutePlanner | ||
| import org.jetbrains.annotations.TestOnly | ||
| import software.aws.toolkits.core.utils.getLogger | ||
| import software.aws.toolkits.core.utils.warn | ||
| import software.aws.toolkits.core.utils.writeText | ||
|
||
| import java.net.URI | ||
| import java.security.KeyStore | ||
| import java.security.cert.CertPathBuilder | ||
| import java.security.cert.CertStore | ||
| import java.security.cert.Certificate | ||
| import java.security.cert.CollectionCertStoreParameters | ||
| import java.security.cert.PKIXBuilderParameters | ||
| import java.security.cert.PKIXCertPathBuilderResult | ||
| import java.security.cert.X509CertSelector | ||
| import java.security.cert.X509Certificate | ||
| import java.util.Base64 | ||
| import kotlin.collections.ifEmpty | ||
|
|
||
| object TrustChainUtil { | ||
|
||
| private val LOG = getLogger<TrustChainUtil>() | ||
|
|
||
| @TestOnly | ||
| fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: Collection<X509Certificate>) = resolveTrustChain(certs, keystoreFromCertificates(trustAnchors)) | ||
|
|
||
| /** | ||
| * Build and validate the complete certificate chain | ||
| * @param certs The end-entity certificate | ||
| * @param trustAnchors The truststore containing trusted CA certificates | ||
| * @return The complete certificate chain | ||
| */ | ||
| fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: KeyStore): List<X509Certificate> { | ||
|
Check notice on line 43 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt
|
||
|
||
| try { | ||
| // Create the selector for the certificate | ||
| val selector = X509CertSelector() | ||
| selector.certificate = certs.first() | ||
|
|
||
| // Create the parameters for path validation | ||
| val pkixParams = PKIXBuilderParameters(trustAnchors, selector) | ||
|
|
||
| // Disable CRL checking since we just want to build the path | ||
| pkixParams.isRevocationEnabled = false | ||
|
|
||
| // Create a CertStore containing the certificate we want to validate | ||
| val ccsp = CollectionCertStoreParameters(certs) | ||
| val certStore = CertStore.getInstance("Collection", ccsp) | ||
| pkixParams.addCertStore(certStore) | ||
|
|
||
| // Get the certification path | ||
| val builder = CertPathBuilder.getInstance("PKIX") | ||
| val result = builder.build(pkixParams) as PKIXCertPathBuilderResult | ||
| val certPath = result.certPath | ||
| val chain = (certPath.certificates as List<X509Certificate>).toMutableList() | ||
|
|
||
| // Add the trust anchor (root CA) to complete the chain | ||
| val trustAnchorCert = result.trustAnchor.trustedCert | ||
| if (trustAnchorCert != null) { | ||
| chain.add(trustAnchorCert) | ||
| } | ||
|
|
||
| return chain | ||
| } catch (e: Exception) { | ||
| // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS | ||
| LOG.warn(e) { "Could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" } | ||
|
|
||
| return emptyList() | ||
| } | ||
| } | ||
|
|
||
| fun getTrustChain(uri: URI): List<X509Certificate> { | ||
|
||
| val proxyProvider = JdkProxyProvider.getInstance() | ||
| var peerCerts: Array<Certificate> = emptyArray() | ||
| val verifierDelegate = DefaultHostnameVerifier() | ||
| val client = HttpClientBuilder.create() | ||
| .setRoutePlanner(SystemDefaultRoutePlanner(proxyProvider.proxySelector)) | ||
| .setDefaultCredentialsProvider(SystemDefaultCredentialsProvider()) | ||
| .setSSLHostnameVerifier { hostname, sslSession -> | ||
| peerCerts = sslSession.peerCertificates | ||
|
|
||
| verifierDelegate.verify(hostname, sslSession) | ||
| } | ||
| // prompt user via modal to accept certificate if needed; otherwise need to prompt separately prior to launching flare | ||
| .setSSLContext(CertificateManager.getInstance().sslContext) | ||
|
|
||
| // client request will fail if user did not accept cert | ||
| client.build().use { it.execute(RequestBuilder.options(uri).build()) } | ||
|
|
||
| val certificates = peerCerts as Array<X509Certificate> | ||
|
|
||
| // java default + custom system | ||
| // excluding leaf cert for case where user has both leaf and issuing CA as trusted roots | ||
| val allAccepted = CertificateManager.getInstance().trustManager.acceptedIssuers.toSet() - certificates.first() | ||
| val ks = keystoreFromCertificates(allAccepted) | ||
|
|
||
| // if this throws then there is a bug because it passed PKIX validation in apache client | ||
| val trustChain = try { | ||
| resolveTrustChain(certificates.toList(), ks) | ||
| } catch (e: Exception) { | ||
| // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS | ||
| LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not root" } | ||
| emptyList() | ||
| } | ||
|
|
||
| // if trust chain is empty, then somehow user only trusts the leaf cert??? | ||
| return trustChain.ifEmpty { | ||
| // so return the served certificate chain from the server and hope that works | ||
| certificates.toList() | ||
| } | ||
| } | ||
|
|
||
| fun certsToPem(certs: List<X509Certificate>): String = | ||
| buildList { | ||
| certs.forEach { | ||
| add("-----BEGIN CERTIFICATE-----") | ||
| add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded)) | ||
| add("-----END CERTIFICATE-----") | ||
| } | ||
| }.joinToString(separator = System.lineSeparator()) | ||
|
|
||
| private fun keystoreFromCertificates(certificates: Collection<X509Certificate>): KeyStore { | ||
| val ks = KeyStore.getInstance(KeyStore.getDefaultType()) | ||
| ks.load(null, null) | ||
| certificates.forEachIndexed { index, cert -> | ||
| ks.setCertificateEntry( | ||
| cert.getSubjectX500Principal().toString() + "-" + DigestUtil.sha256Hex(cert.encoded), | ||
|
Check notice on line 136 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt
|
||
|
||
| cert | ||
| ) | ||
| } | ||
| return ks | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.