Files
pangolin/client/lib/services/connect_api.dart
T
wangjia 43b25c8aa0
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
feat: 国内流量直连分流(geoip-cn / geosite-cn)— #5
国内 IP/域名直连(不走隧道)→ 省流量 + 国内访问快;非国内走代理。

- clientconfig.go: BuildClientConfig 加 ClientConfigOpts{SplitCN,RulesBaseURL};
  开启时 route 加 {rule_set:[geoip-cn,geosite-cn]→direct} + 定义 rule_set
  (remote .srs,download_detour:direct 直连下载)
- rules.go: 控制面静态服务 /v1/rules/{name}.srs(白名单防穿越)——自托管避免
  GitHub 在国内被墙的鸡生蛋;客户端反正连控制面,可达性有保证
- nodes.go ConnectNode: 读 ?split_cn → opts;NodeAPI 加 rulesBaseURL
  (PANGOLIN_PUBLIC_URL);main.go 挂 /v1/rules 路由 + RulesHandler
- 客户端: connect_api splitCN→?split_cn=1;connection_provider 传 smartRoute 偏好
- deploy/single-node: 拉 geoip-cn/geosite-cn.srs 到 $DATA_DIR/rules +
  设 PANGOLIN_PUBLIC_URL/PANGOLIN_RULES_DIR

验证:go test(splitCN 开/关渲染 + RulesHandler 白名单/404)+ flutter analyze +
shellcheck;不需要节点。订阅链接暂用默认 opts、DNS 分流为后续增强。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 20:34:00 +08:00

103 lines
3.3 KiB
Dart
Raw 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.
// connect_api.dart — 控制面 connect 接口客户端
//
// 职责:POST /v1/nodes/:id/connect → 获取完整 sing-box config JSON。
// 关键约束(§3.1):返回的 JSON 字符串必须原样透传给 VpnBridge.start
// 调用方禁止对字符串做任何增删改;本类同样不修改响应体。
//
// M6 联调:mock 服务器地址由 --dart-define=PANGOLIN_API_URL 注入(默认 :8081)。
// tsk_nuoKSM4Vt-zK
import 'dart:convert';
import 'package:http/http.dart' as http;
/// 控制面接口调用失败时抛出。
/// [statusCode] < 0 表示网络层错误,-2 表示响应体 JSON 解析失败。
class ConnectApiException implements Exception {
const ConnectApiException({
required this.statusCode,
required this.messageZh,
required this.messageEn,
});
final int statusCode;
final String messageZh;
final String messageEn;
@override
String toString() => 'ConnectApiException($statusCode): $messageZh';
}
/// [ConnectApi] 封装 POST /v1/nodes/:id/connect 调用。
///
/// 可通过 [client] 注入自定义 [http.Client](方便单测使用 MockClient)。
class ConnectApi {
ConnectApi({
required this.baseUrl,
required this.authToken,
http.Client? client,
}) : _client = client ?? http.Client();
final String baseUrl;
final String authToken;
final http.Client _client;
/// 向控制面请求 [nodeId] 节点的完整 sing-box config JSON。
///
/// 返回**原始响应体字符串**,调用方必须不做任何修改直接传入 `VpnBridge.start`。
/// 异常:[ConnectApiException]HTTP 非 200、超时、非法 JSON)。
Future<String> fetchConfig({
required String nodeId,
required String deviceId,
bool splitCN = false,
}) async {
// splitCN=true 时带 ?split_cn=1:控制面渲染国内 IP/域名直连(不走隧道,#5)。
var uri = Uri.parse('$baseUrl/v1/nodes/$nodeId/connect');
if (splitCN) {
uri = uri.replace(queryParameters: {'split_cn': '1'});
}
final http.Response response;
try {
response = await _client
.post(
uri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $authToken',
},
body: jsonEncode({'device_id': deviceId}),
)
.timeout(const Duration(seconds: 15));
} on Exception catch (e) {
throw ConnectApiException(
statusCode: -1,
messageZh: '网络请求失败,请检查连接后重试',
messageEn: 'Network error: $e',
);
}
if (response.statusCode != 200) {
throw ConnectApiException(
statusCode: response.statusCode,
messageZh: '节点连接失败 (HTTP ${response.statusCode})',
messageEn: 'Connect failed (HTTP ${response.statusCode})',
);
}
// 仅做合法性校验,不修改响应体。
try {
jsonDecode(response.body);
} catch (_) {
throw const ConnectApiException(
statusCode: -2,
messageZh: '收到的配置格式无效,请稍后重试',
messageEn: 'Invalid config JSON received from server',
);
}
// 原样返回——调用方透传给 VpnBridge.start,不得在途中修改。
return response.body;
}
void dispose() => _client.close();
}