Files
xmz7891 b62ac865ea 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 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
2026-10-06 19:01:05 +08:00

475 lines
16 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}