mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
272 lines
12 KiB
Java
272 lines
12 KiB
Java
package com.lanremote.watch;
|
||
|
||
import java.io.ByteArrayInputStream;
|
||
import java.io.IOException;
|
||
import java.net.HttpURLConnection;
|
||
import java.net.URL;
|
||
import java.security.KeyStore;
|
||
import java.security.cert.CertificateFactory;
|
||
import java.security.cert.X509Certificate;
|
||
|
||
import javax.net.ssl.HttpsURLConnection;
|
||
import javax.net.ssl.SSLContext;
|
||
import javax.net.ssl.SSLHandshakeException;
|
||
import javax.net.ssl.SSLSocketFactory;
|
||
import javax.net.ssl.TrustManager;
|
||
import javax.net.ssl.TrustManagerFactory;
|
||
import javax.net.ssl.X509TrustManager;
|
||
|
||
/**
|
||
* TLS 兼容层:让老手表也能连上中转。
|
||
*
|
||
* <p>为什么需要:
|
||
* 手表端报 {@code SSLHandshakeException: Chain validation failed}。
|
||
* 中转的证书链是
|
||
* {@code relay.xxmxxs.top <- GTS WE1 <- GTS Root R4 <- GlobalSign Root CA},
|
||
* 而很多廉价手表的系统根证书库缺 GTS Root R4(AOSP 2021 年才加),
|
||
* 于是找不到信任锚,握手直接失败。
|
||
*
|
||
* <p>做法:
|
||
* 把这两个根证书内嵌进 APK,校验时<b>系统库或内置库任一通过即可</b>。
|
||
* 这两个都是公开的合法根证书,<b>不是跳过校验</b>:
|
||
* 证书仍必须由可信 CA 签发、且域名匹配,只是把可信 CA 的集合
|
||
* 从"手表自带的那一份"扩大到"手表自带的 + 这两个"。
|
||
*
|
||
* <p>同时把难懂的异常翻译成用户能看懂的话。
|
||
*/
|
||
public final class TlsCompat {
|
||
|
||
private TlsCompat() {
|
||
}
|
||
|
||
// ---------------- 内置的根证书 ----------------
|
||
|
||
/** Google Trust Services Root R4(自签名根,EC P-384)。 */
|
||
private static final String GTS_ROOT_R4 =
|
||
"-----BEGIN CERTIFICATE-----\n" +
|
||
"MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD\n" +
|
||
"VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG\n" +
|
||
"A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw\n" +
|
||
"WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz\n" +
|
||
"IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n" +
|
||
"AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi\n" +
|
||
"QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR\n" +
|
||
"HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" +
|
||
"BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D\n" +
|
||
"9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8\n" +
|
||
"p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD\n" +
|
||
"-----END CERTIFICATE-----\n";
|
||
|
||
/** GlobalSign Root CA(自签名根,RSA 2048,1998 年起就在各系统里)。 */
|
||
private static final String GLOBALSIGN_ROOT_CA =
|
||
"-----BEGIN CERTIFICATE-----\n" +
|
||
"MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\n" +
|
||
"A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\n" +
|
||
"b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\n" +
|
||
"MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\n" +
|
||
"YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\n" +
|
||
"aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\n" +
|
||
"jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\n" +
|
||
"xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n" +
|
||
"1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\n" +
|
||
"snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\n" +
|
||
"U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n" +
|
||
"9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\n" +
|
||
"BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\n" +
|
||
"AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\n" +
|
||
"yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n" +
|
||
"38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\n" +
|
||
"AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\n" +
|
||
"DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\n" +
|
||
"HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n" +
|
||
"-----END CERTIFICATE-----\n";
|
||
|
||
private static SSLSocketFactory factory;
|
||
private static X509TrustManager bundled;
|
||
private static boolean ready;
|
||
private static boolean initTried;
|
||
|
||
// ---------------- 对外 ----------------
|
||
|
||
/**
|
||
* 打开一个连接,HTTPS 时挂上兼容的 SSLSocketFactory。
|
||
*
|
||
* <p>HTTP(局域网中转)不走这里,原样返回。
|
||
*/
|
||
public static HttpURLConnection open(String urlText) throws IOException {
|
||
HttpURLConnection connection =
|
||
(HttpURLConnection) new URL(urlText).openConnection();
|
||
connection.setUseCaches(false);
|
||
if (connection instanceof HttpsURLConnection) {
|
||
SSLSocketFactory ssl = factory();
|
||
if (ssl != null) {
|
||
((HttpsURLConnection) connection).setSSLSocketFactory(ssl);
|
||
}
|
||
}
|
||
return connection;
|
||
}
|
||
|
||
/** 拿到兼容的 SSLSocketFactory;准备失败就返回 null(退回系统默认)。 */
|
||
public static synchronized SSLSocketFactory factory() {
|
||
if (!initTried) {
|
||
initTried = true;
|
||
try {
|
||
KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
|
||
store.load(null, null);
|
||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||
int index = 0;
|
||
for (String pem : new String[]{GTS_ROOT_R4, GLOBALSIGN_ROOT_CA}) {
|
||
X509Certificate cert = (X509Certificate) cf.generateCertificate(
|
||
new ByteArrayInputStream(pem.getBytes("UTF-8")));
|
||
store.setCertificateEntry("lanremote-ca-" + (index++), cert);
|
||
}
|
||
|
||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||
TrustManagerFactory.getDefaultAlgorithm());
|
||
tmf.init(store);
|
||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||
if (tm instanceof X509TrustManager) {
|
||
bundled = (X509TrustManager) tm;
|
||
break;
|
||
}
|
||
}
|
||
|
||
SSLContext context = SSLContext.getInstance("TLS");
|
||
context.init(null, new TrustManager[]{new CompatTrustManager()}, null);
|
||
factory = context.getSocketFactory();
|
||
ready = true;
|
||
} catch (Throwable error) {
|
||
// 准备失败就退回系统默认,不影响正常设备
|
||
factory = null;
|
||
ready = false;
|
||
}
|
||
}
|
||
return ready ? factory : null;
|
||
}
|
||
|
||
/**
|
||
* 把 TLS / 网络异常翻译成用户能看懂的一句话。
|
||
*
|
||
* <p>手表上原本只会显示一长串英文类名,用户完全不知道该怎么办。
|
||
*/
|
||
public static String explain(Throwable error) {
|
||
if (error == null) {
|
||
return "未知错误";
|
||
}
|
||
String text = String.valueOf(error);
|
||
String lower = text.toLowerCase();
|
||
|
||
if (lower.contains("chain validation failed")
|
||
|| lower.contains("trust anchor")
|
||
|| lower.contains("certpathvalidator")) {
|
||
return "证书校验失败(中转的根证书手表不认识)。"
|
||
+ "可能是手表系统太旧,或网络被劫持。";
|
||
}
|
||
if (lower.contains("not yet valid") || lower.contains("expired")
|
||
|| lower.contains("certificate expired")) {
|
||
return "证书时间不对 —— 把手表的【日期和时间】调成自动获取,"
|
||
+ "再试一次。";
|
||
}
|
||
if (lower.contains("timeout") || lower.contains("timed out")) {
|
||
return "连接超时(网络太慢或被挡住)";
|
||
}
|
||
if (lower.contains("unable to resolve host")
|
||
|| lower.contains("unknownhost")) {
|
||
return "域名解析不了(检查手表能不能上网)";
|
||
}
|
||
if (lower.contains("connection refused")) {
|
||
return "对方拒绝了连接(电脑上 ykxmz 没开?)";
|
||
}
|
||
if (lower.contains("network is unreachable")
|
||
|| lower.contains("no route")) {
|
||
return "手表没联网(检查 WiFi)";
|
||
}
|
||
if (lower.contains("eof") || lower.contains("reset")) {
|
||
return "连接被中断(网络不稳)";
|
||
}
|
||
if (text.length() > 80) {
|
||
return text.substring(0, 80);
|
||
}
|
||
return text;
|
||
}
|
||
|
||
/**
|
||
* 组合信任:系统默认 或 内置根证书,任一通过即认可。
|
||
*
|
||
* <p>这是修这个问题的关键 ——
|
||
* 老手表缺 GTS Root R4,但内置的那份有,于是校验通过;
|
||
* 新设备走系统默认照样通过。两边都不削弱安全性。
|
||
*/
|
||
private static final class CompatTrustManager implements X509TrustManager {
|
||
|
||
private final X509TrustManager system = findSystem();
|
||
|
||
private static X509TrustManager findSystem() {
|
||
try {
|
||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||
TrustManagerFactory.getDefaultAlgorithm());
|
||
tmf.init((KeyStore) null); // null = 系统默认
|
||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||
if (tm instanceof X509TrustManager) {
|
||
return (X509TrustManager) tm;
|
||
}
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 拿不到就算了,下面用内置的
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@Override
|
||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||
throws java.security.cert.CertificateException {
|
||
if (system != null) {
|
||
system.checkClientTrusted(chain, authType);
|
||
return;
|
||
}
|
||
if (bundled != null) {
|
||
bundled.checkClientTrusted(chain, authType);
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||
throws java.security.cert.CertificateException {
|
||
java.security.cert.CertificateException first = null;
|
||
if (system != null) {
|
||
try {
|
||
system.checkServerTrusted(chain, authType);
|
||
return;
|
||
} catch (java.security.cert.CertificateException error) {
|
||
first = error;
|
||
}
|
||
}
|
||
if (bundled != null) {
|
||
try {
|
||
bundled.checkServerTrusted(chain, authType);
|
||
return;
|
||
} catch (java.security.cert.CertificateException error) {
|
||
if (first == null) {
|
||
first = error;
|
||
}
|
||
}
|
||
}
|
||
throw (first != null ? first
|
||
: new java.security.cert.CertificateException(
|
||
"证书校验失败"));
|
||
}
|
||
|
||
@Override
|
||
public X509Certificate[] getAcceptedIssuers() {
|
||
X509Certificate[] a = system == null
|
||
? new X509Certificate[0] : system.getAcceptedIssuers();
|
||
X509Certificate[] b = bundled == null
|
||
? new X509Certificate[0] : bundled.getAcceptedIssuers();
|
||
X509Certificate[] all = new X509Certificate[a.length + b.length];
|
||
System.arraycopy(a, 0, all, 0, a.length);
|
||
System.arraycopy(b, 0, all, a.length, b.length);
|
||
return all;
|
||
}
|
||
}
|
||
}
|