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}