ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性

被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+474
View File
@@ -0,0 +1,474 @@
package com.lanremote.watch;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 极简 JSON 编解码器。
*
* 刻意不依赖 org.json / Gson:这样协议层是**纯 Java**,
* 既能在安卓上跑,也能在桌面 JVM 上做跨语言联调测试。
*/
public final class Json {
private Json() {
}
// ------------------------------------------------------------------
// 构造辅助
// ------------------------------------------------------------------
public static Map<String, Object> obj() {
return new LinkedHashMap<String, Object>();
}
public static Map<String, Object> obj(Object... kv) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
for (int i = 0; i + 1 < kv.length; i += 2) {
map.put(String.valueOf(kv[i]), kv[i + 1]);
}
return map;
}
public static String str(Map<String, Object> map, String key, String fallback) {
Object value = map == null ? null : map.get(key);
return value == null ? fallback : String.valueOf(value);
}
public static int integer(Map<String, Object> map, String key, int fallback) {
Object value = map == null ? null : map.get(key);
if (value instanceof Number) {
return ((Number) value).intValue();
}
if (value == null) {
return fallback;
}
try {
return (int) Double.parseDouble(String.valueOf(value));
} catch (NumberFormatException e) {
return fallback;
}
}
public static boolean bool(Map<String, Object> map, String key, boolean fallback) {
Object value = map == null ? null : map.get(key);
if (value instanceof Boolean) {
return (Boolean) value;
}
if (value == null) {
return fallback;
}
return Boolean.parseBoolean(String.valueOf(value));
}
@SuppressWarnings("unchecked")
/**
* 取一个字符串数组。元素不是字符串的会被跳过。
*
* 中转会把被控端上报的局域网 IP 列表放在这里(lanIps),
* 手表用它判断能不能走局域网直连。
*/
public static List<String> stringList(Map<String, Object> map, String key) {
List<String> out = new java.util.ArrayList<String>();
List<Object> raw = list(map, key);
if (raw == null) {
return out;
}
for (Object item : raw) {
if (item instanceof String) {
String text = ((String) item).trim();
if (!text.isEmpty()) {
out.add(text);
}
}
}
return out;
}
public static List<Object> list(Map<String, Object> map, String key) {
Object value = map == null ? null : map.get(key);
if (value instanceof List) {
return (List<Object>) value;
}
return new ArrayList<Object>();
}
@SuppressWarnings("unchecked")
public static Map<String, Object> asMap(Object value) {
return value instanceof Map ? (Map<String, Object>) value : null;
}
// ------------------------------------------------------------------
// 编码
// ------------------------------------------------------------------
/**
* 把字符串转义成能安全塞进 JSON 字符串字面量的形式。
* 只处理引号、反斜杠和控制字符 —— 中文不用转,JSON 本身是 UTF-8。
*/
public static String escape(String text) {
if (text == null) {
return "";
}
StringBuilder out = new StringBuilder(text.length() + 16);
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case '"':
out.append("\\\"");
break;
case '\\':
out.append("\\\\");
break;
case '\n':
out.append("\\n");
break;
case '\r':
out.append("\\r");
break;
case '\t':
out.append("\\t");
break;
default:
if (ch < 0x20) {
out.append(String.format("\\u%04x", (int) ch));
} else {
out.append(ch);
}
break;
}
}
return out.toString();
}
public static String write(Object value) {
StringBuilder sb = new StringBuilder(256);
writeValue(sb, value);
return sb.toString();
}
private static void writeValue(StringBuilder sb, Object value) {
if (value == null) {
sb.append("null");
} else if (value instanceof String) {
writeString(sb, (String) value);
} else if (value instanceof Boolean) {
sb.append(((Boolean) value) ? "true" : "false");
} else if (value instanceof Double || value instanceof Float) {
double d = ((Number) value).doubleValue();
if (d == Math.rint(d) && !Double.isInfinite(d)) {
sb.append((long) d);
} else {
sb.append(d);
}
} else if (value instanceof Number) {
sb.append(((Number) value).longValue());
} else if (value instanceof Map) {
writeObject(sb, (Map<?, ?>) value);
} else if (value instanceof Iterable) {
writeArray(sb, (Iterable<?>) value);
} else if (value instanceof byte[]) {
writeString(sb, new String((byte[]) value, java.nio.charset.Charset.forName("UTF-8")));
} else {
writeString(sb, String.valueOf(value));
}
}
private static void writeObject(StringBuilder sb, Map<?, ?> map) {
sb.append('{');
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!first) {
sb.append(',');
}
first = false;
writeString(sb, String.valueOf(entry.getKey()));
sb.append(':');
writeValue(sb, entry.getValue());
}
sb.append('}');
}
private static void writeArray(StringBuilder sb, Iterable<?> items) {
sb.append('[');
boolean first = true;
for (Object item : items) {
if (!first) {
sb.append(',');
}
first = false;
writeValue(sb, item);
}
sb.append(']');
}
private static void writeString(StringBuilder sb, String text) {
sb.append('"');
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
switch (c) {
case '"': sb.append("\\\""); break;
case '\\': sb.append("\\\\"); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
case '\b': sb.append("\\b"); break;
case '\f': sb.append("\\f"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
sb.append('"');
}
// ------------------------------------------------------------------
// 解码
// ------------------------------------------------------------------
public static Object parse(String text) {
Parser parser = new Parser(text);
parser.skipWhitespace();
Object value = parser.readValue();
parser.skipWhitespace();
if (!parser.atEnd()) {
throw new IllegalArgumentException("JSON 尾部存在多余内容");
}
return value;
}
public static Map<String, Object> parseObject(String text) {
Object value = parse(text);
Map<String, Object> map = asMap(value);
if (map == null) {
throw new IllegalArgumentException("期望 JSON 对象");
}
return map;
}
private static final class Parser {
/** 最大嵌套深度。不加限制的话,一行 "[[[[…" 就能把栈撑爆,
* 而 UDP 发现的入口是局域网里任何主机都能打的,等于远程 DoS。 */
private static final int MAX_DEPTH = 64;
private final String src;
private int pos;
private int depth;
Parser(String src) {
this.src = src;
}
boolean atEnd() {
return pos >= src.length();
}
void skipWhitespace() {
while (pos < src.length()) {
char c = src.charAt(pos);
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
pos++;
} else {
break;
}
}
}
Object readValue() {
if (atEnd()) {
throw new IllegalArgumentException("JSON 意外结束");
}
if (depth > MAX_DEPTH) {
throw new IllegalArgumentException("JSON 嵌套太深");
}
char c = src.charAt(pos);
switch (c) {
case '{': return readObject();
case '[': return readArray();
case '"': return readString();
case 't':
expect("true");
return Boolean.TRUE;
case 'f':
expect("false");
return Boolean.FALSE;
case 'n':
expect("null");
return null;
default:
return readNumber();
}
}
private void expect(String literal) {
if (!src.startsWith(literal, pos)) {
throw new IllegalArgumentException("位置 " + pos + " 处 JSON 非法");
}
pos += literal.length();
}
private Map<String, Object> readObject() {
if (depth >= MAX_DEPTH) {
throw new IllegalArgumentException("JSON 嵌套太深");
}
depth++;
try {
return readObjectInner();
} finally {
// 必须配对减去!只加不减会让计数器单调递增,
// 于是"回复里有 65 个对象"就被误判成嵌套太深,
// 而调用方会静默丢弃这条回复 -> 表现为"连不上电脑"。
depth--;
}
}
private Map<String, Object> readObjectInner() {
Map<String, Object> map = new LinkedHashMap<String, Object>();
pos++; // '{'
skipWhitespace();
if (!atEnd() && src.charAt(pos) == '}') {
pos++;
return map;
}
while (true) {
skipWhitespace();
String key = readString();
skipWhitespace();
if (atEnd() || src.charAt(pos) != ':') {
throw new IllegalArgumentException("对象缺少 ':',位置 " + pos);
}
pos++;
skipWhitespace();
map.put(key, readValue());
skipWhitespace();
if (atEnd()) {
throw new IllegalArgumentException("对象未闭合");
}
char c = src.charAt(pos);
if (c == ',') {
pos++;
continue;
}
if (c == '}') {
pos++;
return map;
}
throw new IllegalArgumentException("对象分隔符非法,位置 " + pos);
}
}
private List<Object> readArray() {
if (depth >= MAX_DEPTH) {
throw new IllegalArgumentException("JSON 嵌套太深");
}
depth++;
try {
return readArrayInner();
} finally {
depth--;
}
}
private List<Object> readArrayInner() {
List<Object> items = new ArrayList<Object>();
pos++; // '['
skipWhitespace();
if (!atEnd() && src.charAt(pos) == ']') {
pos++;
return items;
}
while (true) {
skipWhitespace();
items.add(readValue());
skipWhitespace();
if (atEnd()) {
throw new IllegalArgumentException("数组未闭合");
}
char c = src.charAt(pos);
if (c == ',') {
pos++;
continue;
}
if (c == ']') {
pos++;
return items;
}
throw new IllegalArgumentException("数组分隔符非法,位置 " + pos);
}
}
private String readString() {
if (atEnd() || src.charAt(pos) != '"') {
throw new IllegalArgumentException("期望字符串,位置 " + pos);
}
pos++;
StringBuilder sb = new StringBuilder(64);
while (true) {
if (atEnd()) {
throw new IllegalArgumentException("字符串未闭合");
}
char c = src.charAt(pos++);
if (c == '"') {
return sb.toString();
}
if (c != '\\') {
sb.append(c);
continue;
}
if (atEnd()) {
throw new IllegalArgumentException("转义字符未完成");
}
char esc = src.charAt(pos++);
switch (esc) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'u':
if (pos + 4 > src.length()) {
throw new IllegalArgumentException("\\u 转义不完整");
}
sb.append((char) Integer.parseInt(src.substring(pos, pos + 4), 16));
pos += 4;
break;
default:
throw new IllegalArgumentException("未知转义 \\" + esc);
}
}
}
private Object readNumber() {
int start = pos;
while (pos < src.length()) {
char c = src.charAt(pos);
if ((c >= '0' && c <= '9') || c == '-' || c == '+' || c == '.'
|| c == 'e' || c == 'E') {
pos++;
} else {
break;
}
}
String token = src.substring(start, pos);
if (token.isEmpty()) {
throw new IllegalArgumentException("非法数字,位置 " + start);
}
if (token.indexOf('.') < 0 && token.indexOf('e') < 0 && token.indexOf('E') < 0) {
try {
return Long.valueOf(token);
} catch (NumberFormatException ignored) {
// 落到 double
}
}
return Double.valueOf(token);
}
}
}