001/** 002 * Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.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.gnu.org/licenses/lgpl-3.0.txt 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 dev.tinyflow.core.util; 017 018import okhttp3.OkHttpClient; 019 020import javax.net.ssl.SSLContext; 021import javax.net.ssl.SSLSocketFactory; 022import javax.net.ssl.TrustManager; 023import javax.net.ssl.X509TrustManager; 024import java.net.InetSocketAddress; 025import java.net.Proxy; 026import java.security.SecureRandom; 027import java.security.cert.CertificateException; 028import java.security.cert.X509Certificate; 029import java.util.concurrent.TimeUnit; 030import java.util.logging.Level; 031import java.util.logging.Logger; 032 033/** 034 * Utility class for creating and configuring OkHttpClient instances. 035 * <p> 036 * By default, it uses secure TLS settings. Insecure HTTPS (trust-all) can be enabled 037 * via system property {@code tinyflow.okhttp.insecure=true}, but it is strongly 038 * discouraged in production environments. 039 * </p> 040 */ 041public final class OkHttpClientUtil { 042 043 private static final Logger LOGGER = Logger.getLogger(OkHttpClientUtil.class.getName()); 044 045 private static volatile OkHttpClient.Builder customBuilder; 046 private static final Object LOCK = new Object(); 047 048 // Prevent instantiation 049 private OkHttpClientUtil() { 050 throw new UnsupportedOperationException("Utility class"); 051 } 052 053 /** 054 * Sets a custom OkHttpClient.Builder to be used by {@link #buildDefaultClient()}. 055 * This should be called during application initialization. 056 */ 057 public static void setCustomBuilder(OkHttpClient.Builder builder) { 058 if (builder == null) { 059 throw new IllegalArgumentException("Builder must not be null"); 060 } 061 customBuilder = builder; 062 } 063 064 /** 065 * Returns a shared default OkHttpClient instance with reasonable timeouts and optional proxy. 066 * If a custom builder was set via {@link #setCustomBuilder}, it will be used. 067 * <p> 068 * SSL is secure by default. Insecure mode (trust-all) can be enabled via system property: 069 * {@code -Dtinyflow.okhttp.insecure=true} 070 * </p> 071 */ 072 public static OkHttpClient buildDefaultClient() { 073 OkHttpClient.Builder builder = customBuilder; 074 if (builder != null) { 075 return builder.build(); 076 } 077 078 synchronized (LOCK) { 079 // Double-check in case another thread set it while waiting 080 builder = customBuilder; 081 if (builder != null) { 082 return builder.build(); 083 } 084 085 builder = new OkHttpClient.Builder() 086 .connectTimeout(1, TimeUnit.MINUTES) 087 .readTimeout(5, TimeUnit.MINUTES); 088 089 // Optional insecure mode (for development/testing only) 090 if (isInsecureModeEnabled()) { 091 LOGGER.warning("OkHttpClient is running in INSECURE mode (trust-all SSL). " + 092 "This is dangerous and should not be used in production."); 093 enableInsecureSsl(builder); 094 } 095 096 configureProxy(builder); 097 return builder.build(); 098 } 099 } 100 101 private static boolean isInsecureModeEnabled() { 102 return Boolean.parseBoolean(System.getProperty("tinyflow.okhttp.insecure", "false")); 103 } 104 105 106 private static void enableInsecureSsl(OkHttpClient.Builder builder) { 107 try { 108 X509TrustManager trustManager = new X509TrustManager() { 109 @Override 110 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 111 } 112 113 @Override 114 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 115 } 116 117 @Override 118 public X509Certificate[] getAcceptedIssuers() { 119 return new X509Certificate[0]; 120 } 121 }; 122 123 SSLContext sslContext = SSLContext.getInstance("TLS"); 124 sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); 125 SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); 126 127 builder.sslSocketFactory(sslSocketFactory, trustManager) 128 .hostnameVerifier((hostname, session) -> true); 129 } catch (Exception e) { 130 throw new IllegalStateException("Failed to configure insecure SSL for OkHttpClient", e); 131 } 132 } 133 134 private static void configureProxy(OkHttpClient.Builder builder) { 135 String proxyHost = getProxyHost(); 136 String proxyPort = getProxyPort(); 137 138 if (StringUtil.hasText(proxyHost) && StringUtil.hasText(proxyPort)) { 139 try { 140 int port = Integer.parseInt(proxyPort.trim()); 141 InetSocketAddress addr = new InetSocketAddress(proxyHost.trim(), port); 142 builder.proxy(new Proxy(Proxy.Type.HTTP, addr)); 143 LOGGER.fine("Configured HTTP proxy: " + proxyHost + ":" + port); 144 } catch (NumberFormatException e) { 145 LOGGER.log(Level.WARNING, "Invalid proxy port: " + proxyPort, e); 146 } 147 } 148 } 149 150 private static String getProxyHost() { 151 String host = System.getProperty("https.proxyHost"); 152 if (!StringUtil.hasText(host)) { 153 host = System.getProperty("http.proxyHost"); 154 } 155 return host; 156 } 157 158 private static String getProxyPort() { 159 String port = System.getProperty("https.proxyPort"); 160 if (!StringUtil.hasText(port)) { 161 port = System.getProperty("http.proxyPort"); 162 } 163 return port; 164 } 165}