mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 14:50:45 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user