mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge pull request #12214 from Kilo-Org/smart-carp
fix(jetbrains): honor IDE certificate and proxy settings for outbound HTTPS
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Honor JetBrains certificate and proxy settings when downloading the CLI and fetching custom provider models.
|
||||
+77
-6
@@ -1,21 +1,29 @@
|
||||
package ai.kilocode.backend.cli
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.util.net.JdkProxyProvider
|
||||
import com.intellij.util.net.ssl.CertificateManager
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.util.Base64
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.net.Authenticator as JdkAuthenticator
|
||||
|
||||
/**
|
||||
* Factory for the OkHttp clients used by the plugin.
|
||||
*
|
||||
* Mirrors the VS Code architecture:
|
||||
* - [api] client has no call/read timeout (streaming ops like prompt/SSE can run long)
|
||||
* - [appLoad] client has a bounded timeout for startup REST calls
|
||||
* - [health] client has a short 3 s timeout and a small dedicated connection pool
|
||||
* Localhost clients ([api], [appLoad], [health]) talk only to the spawned CLI on
|
||||
* `127.0.0.1`, bundle Basic Auth via an interceptor, and deliberately stay off the
|
||||
* IntelliJ proxy stack so loopback traffic is never routed through a proxy.
|
||||
*
|
||||
* Both clients bundle Basic Auth via an interceptor and are fully independent
|
||||
* of any IntelliJ-platform-provided HTTP stack.
|
||||
* External clients ([cliDownload], [modelFetch]) reach the public internet (GitHub
|
||||
* releases, user-supplied provider URLs) and are wired to the IDE's configured
|
||||
* certificate store and proxy via [externalBuilder] so they work on corporate
|
||||
* networks that MITM TLS or require an authenticated proxy.
|
||||
*/
|
||||
object KiloBackendHttpClients {
|
||||
|
||||
@@ -52,6 +60,46 @@ object KiloBackendHttpClients {
|
||||
.connectionPool(ConnectionPool(1, 30, TimeUnit.SECONDS))
|
||||
.build()
|
||||
|
||||
/** CLI download client — platform TLS/proxy settings for GitHub release traffic. */
|
||||
fun cliDownload(): OkHttpClient = externalBuilder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(120, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
/** Model fetch client — platform TLS/proxy settings for user-supplied provider URLs. */
|
||||
fun modelFetch(): OkHttpClient = externalBuilder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.callTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
/** Derive a per-request bounded client from an existing one, preserving auth/interceptors. */
|
||||
fun bounded(client: OkHttpClient, timeoutSeconds: Long): OkHttpClient {
|
||||
val timeout = timeoutSeconds.coerceAtLeast(1L)
|
||||
return client.newBuilder()
|
||||
.callTimeout(timeout, TimeUnit.SECONDS)
|
||||
.readTimeout(timeout, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for outbound internet requests wired to the IDE certificate store and proxy.
|
||||
*
|
||||
* When no IntelliJ application is available (unit tests, early bootstrap) the platform
|
||||
* services cannot be resolved, so a bare builder is returned unchanged.
|
||||
*/
|
||||
fun externalBuilder(): OkHttpClient.Builder {
|
||||
val builder = OkHttpClient.Builder()
|
||||
ApplicationManager.getApplication() ?: return builder
|
||||
val cert = CertificateManager.getInstance()
|
||||
val proxy = JdkProxyProvider.getInstance()
|
||||
return builder
|
||||
.sslSocketFactory(cert.sslContext.socketFactory, cert.trustManager)
|
||||
.proxySelector(proxy.proxySelector)
|
||||
.proxyAuthenticator(proxyAuth(proxy.authenticator))
|
||||
}
|
||||
|
||||
/** Shut down both dispatcher and connection pool for the given client. */
|
||||
fun shutdown(client: OkHttpClient) {
|
||||
client.dispatcher.executorService.shutdown()
|
||||
@@ -68,4 +116,27 @@ object KiloBackendHttpClients {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Answer proxy 407 challenges using the IDE's proxy credentials, without touching global auth state. */
|
||||
private fun proxyAuth(auth: JdkAuthenticator): okhttp3.Authenticator = okhttp3.Authenticator { route, response ->
|
||||
if (response.code != 407) return@Authenticator null
|
||||
val addr = (route?.proxy ?: Proxy.NO_PROXY).address() as? InetSocketAddress ?: return@Authenticator null
|
||||
val url = response.request.url
|
||||
response.challenges().firstNotNullOfOrNull { challenge ->
|
||||
if (!"Basic".equals(challenge.scheme, ignoreCase = true)) return@firstNotNullOfOrNull null
|
||||
val pwd = auth.requestPasswordAuthenticationInstance(
|
||||
addr.hostString,
|
||||
addr.address,
|
||||
addr.port,
|
||||
url.scheme,
|
||||
challenge.realm,
|
||||
challenge.scheme,
|
||||
url.toUrl(),
|
||||
JdkAuthenticator.RequestorType.PROXY,
|
||||
) ?: return@firstNotNullOfOrNull null
|
||||
response.request.newBuilder()
|
||||
.header("Proxy-Authorization", Credentials.basic(pwd.userName, String(pwd.password), challenge.charset))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -30,11 +30,7 @@ import java.util.zip.ZipInputStream
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class KiloCliDownloader(
|
||||
private val http: OkHttpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(120, TimeUnit.SECONDS)
|
||||
.build(),
|
||||
private val http: OkHttpClient = KiloBackendHttpClients.cliDownload(),
|
||||
private val log: KiloLog = KiloLog.create(KiloCliDownloader::class.java),
|
||||
private val root: File = File(PathManager.getSystemPath(), "kilo/cli"),
|
||||
private val baseUrl: String = "https://github.com/Kilo-Org/kilocode/releases/download",
|
||||
|
||||
+4
-11
@@ -2,6 +2,7 @@ package ai.kilocode.backend.provider
|
||||
|
||||
import ai.kilocode.backend.app.KiloBackendAppService
|
||||
import ai.kilocode.backend.app.LoadError
|
||||
import ai.kilocode.backend.cli.KiloBackendHttpClients
|
||||
import ai.kilocode.backend.cli.KiloCliDataParser
|
||||
import ai.kilocode.backend.rpc.KiloWorkspaceDtoMapper
|
||||
import ai.kilocode.log.KiloLog
|
||||
@@ -21,12 +22,10 @@ import ai.kilocode.rpc.dto.ProviderSettingsDto
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class KiloBackendProviderSettingsManager(
|
||||
private val app: KiloBackendAppService,
|
||||
@@ -34,11 +33,7 @@ internal class KiloBackendProviderSettingsManager(
|
||||
companion object {
|
||||
private val LOG = KiloLog.create(KiloBackendProviderSettingsManager::class.java)
|
||||
private val JSON = "application/json".toMediaType()
|
||||
private val FETCH = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(15, TimeUnit.SECONDS)
|
||||
.callTimeout(15, TimeUnit.SECONDS)
|
||||
.build()
|
||||
private val FETCH by lazy { KiloBackendHttpClients.modelFetch() }
|
||||
private const val CALL_TIMEOUT_SECONDS = 15L
|
||||
private const val OAUTH_CALL_TIMEOUT_SECONDS = 60L
|
||||
}
|
||||
@@ -253,10 +248,8 @@ internal class KiloBackendProviderSettingsManager(
|
||||
private suspend fun request(request: Request, timeoutSeconds: Long = CALL_TIMEOUT_SECONDS): String {
|
||||
val start = System.currentTimeMillis()
|
||||
LOG.debug { "provider settings http: start ${request.method} ${request.url.encodedPath}" }
|
||||
val http = app.http?.newBuilder()
|
||||
?.callTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||
?.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
|
||||
?.build() ?: throw IllegalStateException("Kilo HTTP client is unavailable")
|
||||
val http = app.http?.let { KiloBackendHttpClients.bounded(it, timeoutSeconds) }
|
||||
?: throw IllegalStateException("Kilo HTTP client is unavailable")
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
http.newCall(request.newBuilder().header("Accept", "application/json").build()).execute().use { response ->
|
||||
|
||||
+52
@@ -98,4 +98,56 @@ class KiloBackendHttpClientsTest {
|
||||
KiloBackendHttpClients.shutdown(client)
|
||||
assertEquals(0, client.connectionPool.connectionCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cli download client keeps release download timeouts`() {
|
||||
val client = KiloBackendHttpClients.cliDownload()
|
||||
try {
|
||||
assertEquals(30_000, client.connectTimeoutMillis)
|
||||
assertEquals(120_000, client.readTimeoutMillis)
|
||||
assertEquals(120_000, client.writeTimeoutMillis)
|
||||
assertEquals(0, client.callTimeoutMillis)
|
||||
} finally {
|
||||
KiloBackendHttpClients.shutdown(client)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `model fetch client has bounded 15 second timeouts`() {
|
||||
val client = KiloBackendHttpClients.modelFetch()
|
||||
try {
|
||||
assertEquals(15_000, client.connectTimeoutMillis)
|
||||
assertEquals(15_000, client.readTimeoutMillis)
|
||||
assertEquals(15_000, client.callTimeoutMillis)
|
||||
} finally {
|
||||
KiloBackendHttpClients.shutdown(client)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bounded client applies per request timeout and preserves auth`() {
|
||||
val pwd = "boundedpwd"
|
||||
val server = MockWebServer()
|
||||
server.enqueue(MockResponse().setBody("ok"))
|
||||
server.start()
|
||||
|
||||
val client = KiloBackendHttpClients.api(pwd)
|
||||
val bounded = KiloBackendHttpClients.bounded(client, 7)
|
||||
try {
|
||||
assertEquals(7_000, bounded.callTimeoutMillis)
|
||||
assertEquals(7_000, bounded.readTimeoutMillis)
|
||||
assertEquals(client.connectTimeoutMillis, bounded.connectTimeoutMillis)
|
||||
|
||||
val request = okhttp3.Request.Builder().url(server.url("/global/config")).build()
|
||||
bounded.newCall(request).execute().use { response ->
|
||||
assertEquals(200, response.code)
|
||||
}
|
||||
val recorded = server.takeRequest()
|
||||
val expected = "Basic ${Base64.getEncoder().encodeToString("kilo:$pwd".toByteArray())}"
|
||||
assertEquals(expected, recorded.getHeader("Authorization"))
|
||||
} finally {
|
||||
KiloBackendHttpClients.shutdown(client)
|
||||
server.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user