diff --git a/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java b/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java new file mode 100644 index 0000000..d4b83cc --- /dev/null +++ b/src/main/java/quant/rich/emoney/client/OkHttpClientProvider.java @@ -0,0 +1,188 @@ +package quant.rich.emoney.client; + +import java.io.IOException; +import java.net.Proxy; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.function.Consumer; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.BufferedSource; +import okio.GzipSource; +import okio.Okio; +import quant.rich.emoney.entity.config.ProxyConfig; +import quant.rich.emoney.util.SpringContextHolder; + +/** + * OkHttpClient 提供器 + * @see quant.rich.emoney.entity.config.ProxyConfig + */ +public class OkHttpClientProvider { + + private static volatile ProxyConfig proxyConfig; + + private static ProxyConfig getProxyConfig() { + if (proxyConfig == null) { + synchronized (EmoneyClient.class) { + if (proxyConfig == null) { + proxyConfig = SpringContextHolder.getBean(ProxyConfig.class); + } + } + } + return proxyConfig; + } + + /** + * 根据 ProxyConfig 获取一个 OkHttpClient 实例 + * @return + */ + public static OkHttpClient getInstance() { + return getInstance(null); + } + + /** + * 根据 ProxyConfig 获取一个 OkHttpClient 实例 + * @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据 proxyConfig 情况覆盖 + * @return + */ + public static OkHttpClient getInstance(Consumer builderConsumer) { + ProxyConfig proxyConfig = getProxyConfig(); + return getInstance(proxyConfig.getProxy(), proxyConfig.getIgnoreHttpsVerification(), builderConsumer); + } + + /** + * 根据指定代理和是否忽略 https 证书获取一个 OkHttpClient 实例 + * @param proxy 指定代理 + * @param ignoreHttpsVerification 是否忽略 https 证书 + * @return + */ + public static OkHttpClient getInstance(Proxy proxy, boolean ignoreHttpsVerification) { + return getInstance(proxy, ignoreHttpsVerification, null); + } + + /** + * 根据指定代理、是否忽略 https 证书和额外 builder 设置获取一个 OkHttpClient 实例 + * @param proxy 指定代理 + * @param ignoreHttpsVerification 是否忽略 https 证书 + * @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据其他参数覆盖 + * @return + */ + public static OkHttpClient getInstance(Proxy proxy, boolean ignoreHttpsVerification, Consumer builderConsumer) { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (builderConsumer != null) { + builderConsumer.accept(builder); + } + builder.proxy(proxy); + if (ignoreHttpsVerification) { + builder + .sslSocketFactory(getSSLSocketFactory(), getX509TrustManager()) + .hostnameVerifier(getHostnameVerifier()); + } + builder.addNetworkInterceptor(new GzipResponseInterceptor()); + return builder.build(); + } + + private static HostnameVerifier getHostnameVerifier() { + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + return hostnameVerifier; + } + + private static SSLSocketFactory getSSLSocketFactory() { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, getTrustManager(), new SecureRandom()); + return sslContext.getSocketFactory(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static X509TrustManager getX509TrustManager() { + X509TrustManager trustManager = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); + } + trustManager = (X509TrustManager) trustManagers[0]; + } catch (Exception e) { + e.printStackTrace(); + } + + return trustManager; + } + + private static TrustManager[] getTrustManager() { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + } + }; + return trustAllCerts; + } + + public static class GzipResponseInterceptor implements Interceptor { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Response response = chain.proceed(request); + + // 只有服务器返回了 gzip 编码才处理 + if ("gzip".equalsIgnoreCase(response.header("Content-Encoding"))) { + // 原始响应体 + ResponseBody body = response.body(); + if (body == null) return response; + + // 用 GzipSource 包装原始流,并缓冲 + GzipSource gzippedResponseBody = new GzipSource(body.source()); + BufferedSource unzippedSource = Okio.buffer(gzippedResponseBody); + + // 构造一个新的 ResponseBody,不再带 Content-Encoding/Length + ResponseBody newBody = ResponseBody.create( + unzippedSource, + body.contentType(), + -1L + ); + + // 去掉 Content-Encoding/Length,让后续调用 body().string() 时拿到解压后的内容 + return response.newBuilder() + .removeHeader("Content-Encoding") + .removeHeader("Content-Length") + .body(newBody) + .build(); + } + + return response; + } + } +} diff --git a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java b/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java index dccabce..a7dad90 100644 --- a/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java +++ b/src/main/java/quant/rich/emoney/entity/config/ProxyConfig.java @@ -1,37 +1,23 @@ package quant.rich.emoney.entity.config; +import java.io.IOException; +import java.net.InetSocketAddress; import java.net.Proxy; -import java.time.LocalDateTime; -import java.util.Objects; - -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.springframework.beans.factory.annotation.Autowired; +import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; -import lombok.AccessLevel; import lombok.Data; -import lombok.Getter; -import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; -import quant.rich.emoney.entity.config.DeviceInfoConfig.DeviceInfo; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import quant.rich.emoney.client.OkHttpClientProvider; import quant.rich.emoney.interceptor.EnumOptionsInterceptor.EnumOptions; import quant.rich.emoney.interfaces.ConfigInfo; import quant.rich.emoney.interfaces.IConfig; -import quant.rich.emoney.interfaces.ValidEmoneyRequestConfig; -import quant.rich.emoney.patch.okhttp.PatchOkHttp; -import quant.rich.emoney.util.EncryptUtils; -import quant.rich.emoney.util.SpringContextHolder; -import quant.rich.emoney.util.TextUtils; +import quant.rich.emoney.validator.ProxyConfigValid; /** * 独立出来一个代理设置的原因是后续可能需要做一个代理池,这样的话独立配置比较适合后续扩展 @@ -39,7 +25,7 @@ import quant.rich.emoney.util.TextUtils; @Data @Accessors(chain = true) @Slf4j -@ValidEmoneyRequestConfig +@ProxyConfigValid @ConfigInfo(field = "proxy", name = "代理设置", initDefault = true) public class ProxyConfig implements IConfig { @@ -57,7 +43,7 @@ public class ProxyConfig implements IConfig { /** * 代理端口 */ - private Integer proxyPort = 0; + private Integer proxyPort = 1; /** * 是否忽略 HTTPS 证书校验 @@ -71,5 +57,66 @@ public class ProxyConfig implements IConfig { public ProxyConfig() {} + /** + * 根据配置获取代理 + * @return + */ + @JsonIgnore + public Proxy getProxy() { + if (getProxyType() != null && getProxyType() != Proxy.Type.DIRECT) { + return new Proxy(getProxyType(), + new InetSocketAddress(getProxyHost(), getProxyPort())); + } + return Proxy.NO_PROXY; + } + + /** + * 测试给定的 HTTP 代理是否有效,并验证是否真的走了代理。 + * @param proxyHost 代理 IP,如 "127.0.0.1" + * @param proxyPort 代理端口,如 8888 + * @return 是否有效 + */ + public static boolean isProxyEffective(Proxy proxy, boolean ignoreHttpsVerification) { + synchronized (ProxyConfig.class) { + + // OkHttp 客户端配置 + OkHttpClient client = OkHttpClientProvider.getInstance( + proxy, ignoreHttpsVerification, + builder -> builder + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS)); + + // 使用 httpbin.org/ip 获取当前请求的公网 IP + Request request = new Request.Builder() + .url("https://httpbin.org/ip") + .header("User-Agent", "ProxyVerifier") + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + System.out.println("Request failed with code: " + response.code()); + return false; + } + + String responseBody = response.body().string(); + System.out.println("Response from proxy: " + responseBody); + + // 可在此根据 IP 做进一步验证,例如是否与本地 IP 不同 + return true; + } catch (IOException e) { + System.out.println("Proxy error: " + e.getMessage()); + return false; + } + } + } + + public static void main(String[] args) { + String proxyIp = "127.0.0.1"; + int proxyPort = 7897; + + boolean result = isProxyEffective( + new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyIp, proxyPort)), true); + System.out.println("Proxy is usable: " + result); + } }