github-advanced-security[bot] commented on code in PR #3037: URL: https://github.com/apache/drill/pull/3037#discussion_r3552487670
########## exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ai/AnthropicProvider.java: ########## @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.drill.exec.server.rest.ai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * LLM provider for the Anthropic Claude API. + * Calls /v1/messages with stream:true, translates Anthropic SSE events + * (message_start, content_block_delta, etc.) to the normalized format. + */ +public class AnthropicProvider implements LlmProvider { + + private static final Logger logger = LoggerFactory.getLogger(AnthropicProvider.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final MediaType JSON_TYPE = MediaType.parse("application/json"); + private static final String DEFAULT_ENDPOINT = "https://api.anthropic.com"; + private static final String ANTHROPIC_VERSION = "2023-06-01"; + + @Override + public String getId() { + return "anthropic"; + } + + @Override + public String getDisplayName() { + return "Anthropic Claude"; + } + + @Override + public LlmCallResult streamChatCompletion(LlmConfig config, List<ChatMessage> messages, + List<ToolDefinition> tools, OutputStream out, UsageObserver usageObserver) + throws Exception { + UsageObserver observer = usageObserver != null ? usageObserver : UsageObserver.NOOP; + + // Create HTTP client with enterprise configuration + OkHttpClient httpClient = HttpClientFactory.createClient(config); + + String endpoint = config.getApiEndpoint(); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = DEFAULT_ENDPOINT; + } + if (endpoint.endsWith("/")) { + endpoint = endpoint.substring(0, endpoint.length() - 1); + } + String url = endpoint + "/v1/messages"; + + ObjectNode requestBody = buildRequestBody(config, messages, tools); + + Request request = new Request.Builder() + .url(url) + .post(RequestBody.create(requestBody.toString(), JSON_TYPE)) + .addHeader("Content-Type", "application/json") + .addHeader("x-api-key", config.getApiKey() != null ? config.getApiKey() : "") + .addHeader("anthropic-version", ANTHROPIC_VERSION) + .build(); + + LlmCallResult result = new LlmCallResult(); + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + String errorBody = ""; + ResponseBody body = response.body(); + if (body != null) { + errorBody = body.string(); + } + String errorMsg = "Anthropic API error " + response.code() + ": " + errorBody; + logger.error(errorMsg); + writeSseEvent(out, "error", "{\"message\":" + MAPPER.writeValueAsString(errorMsg) + "}"); + return result; + } + + ResponseBody body = response.body(); + if (body == null) { + writeSseEvent(out, "error", "{\"message\":\"Empty response from Anthropic API\"}"); + return result; + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { + processAnthropicStream(reader, out, result, observer); + } + } + return result; + } + + @Override + public ValidationResult validateConfig(LlmConfig config) { + if (config.getApiKey() == null || config.getApiKey().isEmpty()) { + return ValidationResult.error("API key is required for Anthropic"); + } + if (config.getModel() == null || config.getModel().isEmpty()) { + return ValidationResult.error("Model name is required"); + } + + // Send a minimal, non-streaming request so we surface real failures (bad key, + // unknown model, unreachable endpoint) instead of reporting success blindly. + String endpoint = config.getApiEndpoint(); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = DEFAULT_ENDPOINT; + } + if (endpoint.endsWith("/")) { + endpoint = endpoint.substring(0, endpoint.length() - 1); + } + + ObjectNode probeBody = MAPPER.createObjectNode(); + probeBody.put("model", config.getModel()); + probeBody.put("max_tokens", 1); + ArrayNode probeMessages = probeBody.putArray("messages"); + ObjectNode probeMsg = probeMessages.addObject(); + probeMsg.put("role", "user"); + probeMsg.put("content", "ping"); + + Request request = new Request.Builder() + .url(endpoint + "/v1/messages") Review Comment: ## CodeQL / Server-side request forgery Potential server-side request forgery due to a [user-provided value](1). [Show more details](https://github.com/apache/drill/security/code-scanning/77) ########## exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ai/HttpClientFactory.java: ########## @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.drill.exec.server.rest.ai; + +import okhttp3.Credentials; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; + +/** + * Factory for creating OkHttpClient instances with enterprise configuration. + * Centralizes HTTP client setup including proxies, SSL/TLS, custom headers, and timeouts. + */ +public class HttpClientFactory { + private static final Logger logger = LoggerFactory.getLogger(HttpClientFactory.class); + + // Default timeouts (in seconds) - matching existing hardcoded values + private static final int DEFAULT_CONNECT_TIMEOUT = 30; + private static final int DEFAULT_READ_TIMEOUT = 120; + private static final int DEFAULT_WRITE_TIMEOUT = 30; + + /** + * Create an OkHttpClient configured with all enterprise settings from LlmConfig. + * + * @param config The LLM configuration + * @return Configured OkHttpClient + * @throws Exception if SSL/TLS configuration or file loading fails + */ + public static OkHttpClient createClient(LlmConfig config) throws Exception { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + + // 1. Configure timeouts + int connectTimeout = config.getConnectTimeoutSeconds() != null + ? config.getConnectTimeoutSeconds() : DEFAULT_CONNECT_TIMEOUT; + int readTimeout = config.getReadTimeoutSeconds() != null + ? config.getReadTimeoutSeconds() : DEFAULT_READ_TIMEOUT; + int writeTimeout = config.getWriteTimeoutSeconds() != null + ? config.getWriteTimeoutSeconds() : DEFAULT_WRITE_TIMEOUT; + + builder.connectTimeout(connectTimeout, TimeUnit.SECONDS) + .readTimeout(readTimeout, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS); + + // 2. Configure proxy + if (config.getProxyUrl() != null && !config.getProxyUrl().isEmpty()) { + configureProxy(builder, config); + } + + // 3. Configure SSL/TLS + configureSSL(builder, config); + + // 4. Add custom headers interceptor + if (config.getCustomHeaders() != null && !config.getCustomHeaders().isEmpty()) { + builder.addInterceptor(new CustomHeadersInterceptor(config.getCustomHeaders())); + } + + // 5. Add logging/debugging interceptor (redacts sensitive headers) + if (logger.isDebugEnabled()) { + builder.addInterceptor(new SensitiveHeaderRedactor()); + } + + return builder.build(); + } + + /** + * Configure HTTP proxy with optional authentication. + */ + private static void configureProxy(OkHttpClient.Builder builder, LlmConfig config) { + HttpUrl proxyUrl = HttpUrl.parse(config.getProxyUrl()); + if (proxyUrl == null) { + throw new IllegalArgumentException("Invalid proxy URL: " + config.getProxyUrl()); + } + + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(proxyUrl.host(), proxyUrl.port())); + builder.proxy(proxy); + + // Proxy authentication + if (config.getProxyUsername() != null && !config.getProxyUsername().isEmpty()) { + builder.proxyAuthenticator((route, response) -> { + String credential = Credentials.basic( + config.getProxyUsername(), + config.getProxyPassword() != null ? config.getProxyPassword() : "" + ); + return response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build(); + }); + } + } + + /** + * Configure SSL/TLS with optional custom truststore (CA certificates) and keystore (client certificates). + */ + private static void configureSSL(OkHttpClient.Builder builder, LlmConfig config) + throws Exception { + // Check if custom SSL configuration is needed + boolean hasKeystore = config.getKeystorePath() != null && !config.getKeystorePath().isEmpty(); + boolean hasTruststore = config.getTruststorePath() != null && !config.getTruststorePath().isEmpty(); + + if (hasKeystore || hasTruststore) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + KeyManager[] keyManagers = null; + if (hasKeystore) { + keyManagers = loadKeyManagers(config); + } + + TrustManager[] trustManagers = null; + if (hasTruststore) { + trustManagers = loadTrustManagers(config); + } + + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + + X509TrustManager trustManager = trustManagers != null + ? (X509TrustManager) trustManagers[0] + : getDefaultTrustManager(); + + builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager); + } + + // Disable SSL verification if explicitly set to false (INSECURE - log warning) + if (Boolean.FALSE.equals(config.getVerifySSL())) { + logger.warn("SSL certificate verification is disabled! Use only for testing/development."); + builder.hostnameVerifier((hostname, session) -> true); + } + } + + /** + * Load key managers from keystore for mTLS (mutual TLS / client certificate) support. + */ + private static KeyManager[] loadKeyManagers(LlmConfig config) throws Exception { + String keystorePath = config.getKeystorePath(); + String keystorePassword = config.getKeystorePassword(); + String keystoreType = config.getKeystoreType() != null ? config.getKeystoreType() : "JKS"; + + KeyStore keyStore = KeyStore.getInstance(keystoreType); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + keyStore.load(fis, keystorePassword != null ? keystorePassword.toCharArray() : null); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keystorePassword != null ? keystorePassword.toCharArray() : null); + + return kmf.getKeyManagers(); + } + + /** + * Load trust managers from truststore for custom CA certificate support. + */ + private static TrustManager[] loadTrustManagers(LlmConfig config) throws Exception { + String truststorePath = config.getTruststorePath(); + String truststorePassword = config.getTruststorePassword(); + String truststoreType = config.getTruststoreType() != null ? config.getTruststoreType() : "JKS"; + + KeyStore trustStore = KeyStore.getInstance(truststoreType); + try (FileInputStream fis = new FileInputStream(truststorePath)) { Review Comment: ## CodeQL / Uncontrolled data used in path expression This path depends on a [user-provided value](1). [Show more details](https://github.com/apache/drill/security/code-scanning/80) ########## exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ai/OpenAiCompatibleProvider.java: ########## @@ -0,0 +1,400 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.drill.exec.server.rest.ai; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * LLM provider for OpenAI-compatible APIs (OpenAI, Azure OpenAI, Ollama, etc.). + * Uses OkHttp to call /chat/completions with stream:true, + * reads SSE line-by-line, and normalizes deltas to the common wire format. + */ +public class OpenAiCompatibleProvider implements LlmProvider { + + private static final Logger logger = LoggerFactory.getLogger(OpenAiCompatibleProvider.class); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final MediaType JSON_TYPE = MediaType.parse("application/json"); + private static final String DEFAULT_ENDPOINT = "https://api.openai.com/v1"; + + @Override + public String getId() { + return "openai"; + } + + @Override + public String getDisplayName() { + return "OpenAI Compatible"; + } + + @Override + public LlmCallResult streamChatCompletion(LlmConfig config, List<ChatMessage> messages, + List<ToolDefinition> tools, OutputStream out, UsageObserver usageObserver) + throws Exception { + UsageObserver observer = usageObserver != null ? usageObserver : UsageObserver.NOOP; + + // Create HTTP client with enterprise configuration + OkHttpClient httpClient = HttpClientFactory.createClient(config); + + String endpoint = config.getApiEndpoint(); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = DEFAULT_ENDPOINT; + } + // Remove trailing slash + if (endpoint.endsWith("/")) { + endpoint = endpoint.substring(0, endpoint.length() - 1); + } + String url = endpoint + "/chat/completions"; + + ObjectNode requestBody = buildRequestBody(config, messages, tools); + + Request.Builder reqBuilder = new Request.Builder() + .url(url) + .post(RequestBody.create(requestBody.toString(), JSON_TYPE)) + .addHeader("Content-Type", "application/json"); + + if (config.getApiKey() != null && !config.getApiKey().isEmpty()) { + reqBuilder.addHeader("Authorization", "Bearer " + config.getApiKey()); + } + + Request request = reqBuilder.build(); + + LlmCallResult result = new LlmCallResult(); + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + String errorBody = ""; + ResponseBody body = response.body(); + if (body != null) { + errorBody = body.string(); + } + String errorMsg = "LLM API error " + response.code() + ": " + errorBody; + logger.error(errorMsg); + writeSseEvent(out, "error", "{\"message\":" + MAPPER.writeValueAsString(errorMsg) + "}"); + return result; + } + + ResponseBody body = response.body(); + if (body == null) { + writeSseEvent(out, "error", "{\"message\":\"Empty response from LLM API\"}"); + return result; + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { + processOpenAiStream(reader, out, result, observer); + } + } + return result; + } + + @Override + public ValidationResult validateConfig(LlmConfig config) { + if (config.getApiKey() == null || config.getApiKey().isEmpty()) { + // Ollama doesn't require an API key, so only warn + String endpoint = config.getApiEndpoint(); + if (endpoint != null && !endpoint.contains("localhost") && !endpoint.contains("127.0.0.1")) { + return ValidationResult.error("API key is required for non-local endpoints"); + } + } + if (config.getModel() == null || config.getModel().isEmpty()) { + return ValidationResult.error("Model name is required"); + } + + // Send a minimal, non-streaming request so we surface real failures (bad key, + // unknown model, unreachable endpoint) instead of reporting success blindly. + String endpoint = config.getApiEndpoint(); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = DEFAULT_ENDPOINT; + } + if (endpoint.endsWith("/")) { + endpoint = endpoint.substring(0, endpoint.length() - 1); + } + + ObjectNode probeBody = MAPPER.createObjectNode(); + probeBody.put("model", config.getModel()); + probeBody.put("max_tokens", 1); + ArrayNode probeMessages = probeBody.putArray("messages"); + ObjectNode probeMsg = probeMessages.addObject(); + probeMsg.put("role", "user"); + probeMsg.put("content", "ping"); + + Request.Builder reqBuilder = new Request.Builder() + .url(endpoint + "/chat/completions") Review Comment: ## CodeQL / Server-side request forgery Potential server-side request forgery due to a [user-provided value](1). [Show more details](https://github.com/apache/drill/security/code-scanning/78) ########## exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ai/HttpClientFactory.java: ########## @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.drill.exec.server.rest.ai; + +import okhttp3.Credentials; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.concurrent.TimeUnit; + +/** + * Factory for creating OkHttpClient instances with enterprise configuration. + * Centralizes HTTP client setup including proxies, SSL/TLS, custom headers, and timeouts. + */ +public class HttpClientFactory { + private static final Logger logger = LoggerFactory.getLogger(HttpClientFactory.class); + + // Default timeouts (in seconds) - matching existing hardcoded values + private static final int DEFAULT_CONNECT_TIMEOUT = 30; + private static final int DEFAULT_READ_TIMEOUT = 120; + private static final int DEFAULT_WRITE_TIMEOUT = 30; + + /** + * Create an OkHttpClient configured with all enterprise settings from LlmConfig. + * + * @param config The LLM configuration + * @return Configured OkHttpClient + * @throws Exception if SSL/TLS configuration or file loading fails + */ + public static OkHttpClient createClient(LlmConfig config) throws Exception { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + + // 1. Configure timeouts + int connectTimeout = config.getConnectTimeoutSeconds() != null + ? config.getConnectTimeoutSeconds() : DEFAULT_CONNECT_TIMEOUT; + int readTimeout = config.getReadTimeoutSeconds() != null + ? config.getReadTimeoutSeconds() : DEFAULT_READ_TIMEOUT; + int writeTimeout = config.getWriteTimeoutSeconds() != null + ? config.getWriteTimeoutSeconds() : DEFAULT_WRITE_TIMEOUT; + + builder.connectTimeout(connectTimeout, TimeUnit.SECONDS) + .readTimeout(readTimeout, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS); + + // 2. Configure proxy + if (config.getProxyUrl() != null && !config.getProxyUrl().isEmpty()) { + configureProxy(builder, config); + } + + // 3. Configure SSL/TLS + configureSSL(builder, config); + + // 4. Add custom headers interceptor + if (config.getCustomHeaders() != null && !config.getCustomHeaders().isEmpty()) { + builder.addInterceptor(new CustomHeadersInterceptor(config.getCustomHeaders())); + } + + // 5. Add logging/debugging interceptor (redacts sensitive headers) + if (logger.isDebugEnabled()) { + builder.addInterceptor(new SensitiveHeaderRedactor()); + } + + return builder.build(); + } + + /** + * Configure HTTP proxy with optional authentication. + */ + private static void configureProxy(OkHttpClient.Builder builder, LlmConfig config) { + HttpUrl proxyUrl = HttpUrl.parse(config.getProxyUrl()); + if (proxyUrl == null) { + throw new IllegalArgumentException("Invalid proxy URL: " + config.getProxyUrl()); + } + + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(proxyUrl.host(), proxyUrl.port())); + builder.proxy(proxy); + + // Proxy authentication + if (config.getProxyUsername() != null && !config.getProxyUsername().isEmpty()) { + builder.proxyAuthenticator((route, response) -> { + String credential = Credentials.basic( + config.getProxyUsername(), + config.getProxyPassword() != null ? config.getProxyPassword() : "" + ); + return response.request().newBuilder() + .header("Proxy-Authorization", credential) + .build(); + }); + } + } + + /** + * Configure SSL/TLS with optional custom truststore (CA certificates) and keystore (client certificates). + */ + private static void configureSSL(OkHttpClient.Builder builder, LlmConfig config) + throws Exception { + // Check if custom SSL configuration is needed + boolean hasKeystore = config.getKeystorePath() != null && !config.getKeystorePath().isEmpty(); + boolean hasTruststore = config.getTruststorePath() != null && !config.getTruststorePath().isEmpty(); + + if (hasKeystore || hasTruststore) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + KeyManager[] keyManagers = null; + if (hasKeystore) { + keyManagers = loadKeyManagers(config); + } + + TrustManager[] trustManagers = null; + if (hasTruststore) { + trustManagers = loadTrustManagers(config); + } + + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + + X509TrustManager trustManager = trustManagers != null + ? (X509TrustManager) trustManagers[0] + : getDefaultTrustManager(); + + builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager); + } + + // Disable SSL verification if explicitly set to false (INSECURE - log warning) + if (Boolean.FALSE.equals(config.getVerifySSL())) { + logger.warn("SSL certificate verification is disabled! Use only for testing/development."); + builder.hostnameVerifier((hostname, session) -> true); + } + } + + /** + * Load key managers from keystore for mTLS (mutual TLS / client certificate) support. + */ + private static KeyManager[] loadKeyManagers(LlmConfig config) throws Exception { + String keystorePath = config.getKeystorePath(); + String keystorePassword = config.getKeystorePassword(); + String keystoreType = config.getKeystoreType() != null ? config.getKeystoreType() : "JKS"; + + KeyStore keyStore = KeyStore.getInstance(keystoreType); + try (FileInputStream fis = new FileInputStream(keystorePath)) { Review Comment: ## CodeQL / Uncontrolled data used in path expression This path depends on a [user-provided value](1). [Show more details](https://github.com/apache/drill/security/code-scanning/79) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
