001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.agentsflex.core.model.client;
017
018import com.agentsflex.core.util.StringUtil;
019import okhttp3.*;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023import java.io.IOException;
024import java.net.InetSocketAddress;
025import java.net.Proxy;
026import java.util.concurrent.TimeUnit;
027
028public class OkHttpClientUtil {
029
030    private static final Logger log = LoggerFactory.getLogger(OkHttpClientUtil.class);
031
032    // 系统属性前缀
033    private static final String PREFIX = "okhttp.";
034
035    // 环境变量前缀(大写)
036    private static final String ENV_PREFIX = "OKHTTP_";
037
038    private static volatile OkHttpClient defaultClient;
039    private static volatile OkHttpClient.Builder customBuilder;
040
041    public static void setOkHttpClientBuilder(OkHttpClient.Builder builder) {
042        if (defaultClient != null) {
043            throw new IllegalStateException("OkHttpClient has already been initialized. " +
044                "Please set the builder before first usage.");
045        }
046        customBuilder = builder;
047    }
048
049    public static OkHttpClient buildDefaultClient() {
050        if (defaultClient == null) {
051            synchronized (OkHttpClientUtil.class) {
052                if (defaultClient == null) {
053                    OkHttpClient.Builder builder = customBuilder != null
054                        ? customBuilder
055                        : createDefaultBuilder();
056                    defaultClient = builder.build();
057                    log.debug("OkHttpClient initialized with config: connectTimeout={}s, readTimeout={}s, writeTimeout={}s, " +
058                            "connectionPool(maxIdle={}, keepAlive={}min)",
059                        getConnectTimeout(), getReadTimeout(), getWriteTimeout(),
060                        getMaxIdleConnections(), getKeepAliveMinutes());
061                }
062            }
063        }
064        return defaultClient;
065    }
066
067    private static OkHttpClient.Builder createDefaultBuilder() {
068        OkHttpClient.Builder builder = new OkHttpClient.Builder()
069            .connectTimeout(getConnectTimeout(), TimeUnit.SECONDS)
070            .readTimeout(getReadTimeout(), TimeUnit.SECONDS)
071            .writeTimeout(getWriteTimeout(), TimeUnit.SECONDS)
072            .connectionPool(new ConnectionPool(getMaxIdleConnections(), getKeepAliveMinutes(), TimeUnit.MINUTES));
073
074        configureProxy(builder);
075        return builder;
076    }
077
078    // ==================== 配置读取方法 ====================
079
080    private static int getConnectTimeout() {
081        return getIntConfig("connectTimeout", "CONNECT_TIMEOUT", 60);
082    }
083
084    private static int getReadTimeout() {
085        return getIntConfig("readTimeout", "READ_TIMEOUT", 300);
086    }
087
088    private static int getWriteTimeout() {
089        return getIntConfig("writeTimeout", "WRITE_TIMEOUT", 60);
090    }
091
092    private static int getMaxIdleConnections() {
093        return getIntConfig("connectionPool.maxIdleConnections", "CONNECTION_POOL_MAX_IDLE_CONNECTIONS", 5);
094    }
095
096    private static long getKeepAliveMinutes() {
097        return getLongConfig("connectionPool.keepAliveMinutes", "CONNECTION_POOL_KEEP_ALIVE_MINUTES", 10);
098    }
099
100    private static String getProxyHost() {
101        String host = getPropertyOrEnv("proxy.host", "PROXY_HOST", null);
102        if (StringUtil.hasText(host)) return host.trim();
103
104        // 兼容 Java 标准代理属性(作为 fallback)
105        host = System.getProperty("https.proxyHost");
106        if (StringUtil.hasText(host)) return host.trim();
107
108        host = System.getProperty("http.proxyHost");
109        if (StringUtil.hasText(host)) return host.trim();
110
111        return null;
112    }
113
114    private static String getProxyPort() {
115        String port = getPropertyOrEnv("proxy.port", "PROXY_PORT", null);
116        if (StringUtil.hasText(port)) return port.trim();
117
118        // 兼容 Java 标准代理属性
119        port = System.getProperty("https.proxyPort");
120        if (StringUtil.hasText(port)) return port.trim();
121
122        port = System.getProperty("http.proxyPort");
123        if (StringUtil.hasText(port)) return port.trim();
124
125        return null;
126    }
127
128    // 新增:获取代理用户名
129    private static String getProxyUsername() {
130        String username = getPropertyOrEnv("proxy.username", "PROXY_USERNAME", null);
131        if (StringUtil.hasText(username)) return username.trim();
132
133        // 兼容 Java 标准代理属性
134        username = System.getProperty("https.proxyUser");
135        if (StringUtil.hasText(username)) return username.trim();
136
137        username = System.getProperty("http.proxyUser");
138        if (StringUtil.hasText(username)) return username.trim();
139
140        return null;
141    }
142
143    // 新增:获取代理密码
144    private static String getProxyPassword() {
145        String password = getPropertyOrEnv("proxy.password", "PROXY_PASSWORD", null);
146        if (StringUtil.hasText(password)) return password.trim();
147
148        // 兼容 Java 标准代理属性
149        password = System.getProperty("https.proxyPassword");
150        if (StringUtil.hasText(password)) return password.trim();
151
152        password = System.getProperty("http.proxyPassword");
153        if (StringUtil.hasText(password)) return password.trim();
154
155        return null;
156    }
157
158    // ==================== 工具方法 ====================
159
160    private static int getIntConfig(String sysPropKey, String envKey, int defaultValue) {
161        String value = getPropertyOrEnv(sysPropKey, envKey, null);
162        if (value == null) return defaultValue;
163        try {
164            return Integer.parseInt(value.trim());
165        } catch (NumberFormatException e) {
166            log.warn("Invalid integer value for '{}': '{}'. Using default: {}", fullSysPropKey(sysPropKey), value, defaultValue);
167            return defaultValue;
168        }
169    }
170
171    private static long getLongConfig(String sysPropKey, String envKey, long defaultValue) {
172        String value = getPropertyOrEnv(sysPropKey, envKey, null);
173        if (value == null) return defaultValue;
174        try {
175            return Long.parseLong(value.trim());
176        } catch (NumberFormatException e) {
177            log.warn("Invalid long value for '{}': '{}'. Using default: {}", fullSysPropKey(sysPropKey), value, defaultValue);
178            return defaultValue;
179        }
180    }
181
182    private static String getPropertyOrEnv(String sysPropKey, String envKey, String defaultValue) {
183        // 1. 系统属性优先
184        String value = System.getProperty(fullSysPropKey(sysPropKey));
185        if (value != null) return value;
186
187        // 2. 环境变量
188        value = System.getenv(ENV_PREFIX + envKey);
189        if (value != null) return value;
190
191        return defaultValue;
192    }
193
194    private static String fullSysPropKey(String key) {
195        return PREFIX + key;
196    }
197
198    // ==================== 代理配置 ====================
199
200    private static void configureProxy(OkHttpClient.Builder builder) {
201        String proxyHost = getProxyHost();
202        String proxyPort = getProxyPort();
203
204        if (StringUtil.hasText(proxyHost) && StringUtil.hasText(proxyPort)) {
205            try {
206                int port = Integer.parseInt(proxyPort);
207                InetSocketAddress address = new InetSocketAddress(proxyHost, port);
208                builder.proxy(new Proxy(Proxy.Type.HTTP, address));
209
210                // 配置代理认证
211                String username = getProxyUsername();
212                String password = getProxyPassword();
213
214                if (StringUtil.hasText(username) && StringUtil.hasText(password)) {
215                    configureProxyAuthenticator(builder, username, password);
216                    log.debug("HTTP proxy with authentication configured: {}@{}:{}",
217                        username, proxyHost, port);
218                } else {
219                    log.debug("HTTP proxy configured (no authentication): {}:{}", proxyHost, port);
220                }
221            } catch (NumberFormatException e) {
222                log.warn("Invalid proxy port '{}'. Proxy will be ignored.", proxyPort, e);
223            }
224        }
225    }
226
227    // 配置代理认证器
228    private static void configureProxyAuthenticator(OkHttpClient.Builder builder, String username, String password) {
229        builder.proxyAuthenticator(new Authenticator() {
230            @Override
231            public Request authenticate(Route route, Response response) throws IOException {
232                // 检查是否是代理认证挑战
233                if (response.code() != 407) {
234                    return null; // 不是代理认证,不处理
235                }
236
237                // 如果已经尝试过认证,直接放弃,防止死循环
238                if (response.request().header("Proxy-Authorization") != null) {
239                    log.error("Proxy authentication failed for user: {}", username);
240                    return null;
241                }
242
243                // 生成 Basic 认证凭证 (格式: "Basic base64(username:password)")
244                String credential = Credentials.basic(username, password);
245
246                // 添加 Proxy-Authorization 头并重新发送请求
247                return response.request().newBuilder()
248                    .header("Proxy-Authorization", credential)
249                    .build();
250            }
251        });
252    }
253}