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]

Reply via email to