clash_service.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:ffi' as ffi;
  4. import 'dart:io';
  5. import 'dart:isolate';
  6. import 'package:naiyouwl/main.dart';
  7. import 'package:dart_json_mapper/dart_json_mapper.dart';
  8. import 'package:ffi/ffi.dart';
  9. import 'package:flutter/foundation.dart';
  10. import 'package:flutter/services.dart';
  11. import 'package:path/path.dart';
  12. import 'package:path/path.dart' as p;
  13. import 'package:get/get.dart';
  14. import 'package:kommon/request/request.dart';
  15. import 'package:kommon/tool/sp_util.dart';
  16. import 'package:naiyouwl/clash_generated_bindings.dart';
  17. import 'package:path_provider/path_provider.dart';
  18. import 'package:proxy_manager/proxy_manager.dart';
  19. import 'package:tray_manager/tray_manager.dart';
  20. import '../bean/clash_config_entity.dart';
  21. late NativeLibrary clashFFI;
  22. //android 或者ios
  23. //const mobileChannel = MethodChannel("xxxPlugin");
  24. class ClashService extends GetxService with TrayListener {
  25. // 需要一起改端口
  26. static const clashBaseUrl = "http://127.0.0.1:$clashExtPort";
  27. static const clashExtPort = 22346;
  28. // 运行时
  29. late Directory _clashDirectory;
  30. RandomAccessFile? _clashLock;
  31. // 流量
  32. final uploadRate = 0.0.obs;
  33. final downRate = 0.0.obs;
  34. final yamlConfigs = RxSet<FileSystemEntity>();
  35. final currentYaml = 'config.yaml'.obs;
  36. final proxyStatus = RxMap<String, int>();
  37. // action
  38. static const ACTION_SET_SYSTEM_PROXY = "assr";
  39. static const ACTION_UNSET_SYSTEM_PROXY = "ausr";
  40. static const MAX_ENTRIES = 5;
  41. // default port
  42. static var initializedHttpPort = 0;
  43. static var initializedSockPort = 0;
  44. static var initializedMixedPort = 0;
  45. // config
  46. Rx<ClashConfigEntity?> configEntity = Rx(null);
  47. // log
  48. Stream<dynamic>? logStream;
  49. RxMap<String, dynamic> proxies = RxMap();
  50. RxBool isSystemProxyObs = RxBool(false);
  51. ClashService() {
  52. // load lib
  53. var fullPath = "";
  54. if (Platform.isWindows) {
  55. fullPath = "libclash.dll";
  56. } else if (Platform.isMacOS) {
  57. fullPath = "libclash.dylib";
  58. } else {
  59. fullPath = "libclash.so";
  60. }
  61. final lib = ffi.DynamicLibrary.open(fullPath);
  62. clashFFI = NativeLibrary(lib);
  63. clashFFI.init_native_api_bridge(ffi.NativeApi.initializeApiDLData);
  64. }
  65. Future<ClashService> init() async {
  66. _clashDirectory = await getApplicationSupportDirectory();
  67. final _ = SpUtil.getData('yaml', defValue: currentYaml.value);
  68. initializedHttpPort = SpUtil.getData('http-port', defValue: 12346);
  69. initializedSockPort = SpUtil.getData('socks-port', defValue: 12347);
  70. initializedMixedPort = SpUtil.getData('mixed-port', defValue: 12348);
  71. currentYaml.value = _;
  72. Request.setBaseUrl(clashBaseUrl);
  73. final clashConfigPath = p.join(_clashDirectory.path, "clash");
  74. _clashDirectory = Directory(clashConfigPath);
  75. if (kDebugMode) {
  76. print("fclash work directory: ${_clashDirectory.path}");
  77. }
  78. final clashConf = p.join(_clashDirectory.path, currentYaml.value);
  79. final countryMMdb = p.join(_clashDirectory.path, 'Country.mmdb');
  80. if (!await _clashDirectory.exists()) {
  81. await _clashDirectory.create(recursive: true);
  82. }
  83. // copy executable to directory
  84. final mmdb = await rootBundle.load('assets/tp/clash/Country.mmdb');
  85. // write to clash dir
  86. final mmdbF = File(countryMMdb);
  87. if (!mmdbF.existsSync()) {
  88. await mmdbF.writeAsBytes(mmdb.buffer.asInt8List());
  89. }
  90. final config = await rootBundle.load('assets/tp/clash/config.yaml');
  91. // write to clash dir
  92. final configF = File(clashConf);
  93. if (!configF.existsSync()) {
  94. await configF.writeAsBytes(config.buffer.asInt8List());
  95. }
  96. // create or detect lock file
  97. await _acquireLock(_clashDirectory);
  98. // ffi
  99. clashFFI.set_home_dir(_clashDirectory.path.toNativeUtf8().cast());
  100. clashFFI.clash_init(_clashDirectory.path.toNativeUtf8().cast());
  101. clashFFI.set_config(clashConf.toNativeUtf8().cast());
  102. clashFFI.set_ext_controller(clashExtPort);
  103. if (clashFFI.parse_options() == 0) {
  104. Get.printInfo(info: "parse ok");
  105. }
  106. Future.delayed(Duration.zero, () {
  107. initDaemon();
  108. });
  109. // tray show issue
  110. if (isDesktop) {
  111. trayManager.addListener(this);
  112. }
  113. // wait getx initialize
  114. // Future.delayed(const Duration(seconds: 3), () {
  115. // if (!Platform.isWindows) {
  116. // Get.find<NotificationService>()
  117. // .showNotification("Fclash", "Is running".tr);
  118. // }
  119. // });
  120. return this;
  121. }
  122. void getConfigs() {
  123. yamlConfigs.clear();
  124. final entities = _clashDirectory.listSync();
  125. for (final entity in entities) {
  126. if (entity.path.toLowerCase().endsWith('.yaml') &&
  127. !yamlConfigs.contains(entity)) {
  128. yamlConfigs.add(entity);
  129. Get.printInfo(info: 'detected: ${entity.path}');
  130. }
  131. }
  132. }
  133. Map<String, dynamic> getConnections() {
  134. final connsPtr = clashFFI.get_all_connections().cast<Utf8>();
  135. String connections = connsPtr.toDartString();
  136. // malloc.free(connsPtr);
  137. return json.decode(connections);
  138. }
  139. void closeAllConnections() {
  140. clashFFI.close_all_connections();
  141. }
  142. bool closeConnection(String connectionId) {
  143. final id = connectionId.toNativeUtf8().cast<ffi.Char>();
  144. return clashFFI.close_connection(id) == 1;
  145. }
  146. void getCurrentClashConfig() {
  147. final configPtr = clashFFI.get_configs().cast<Utf8>();
  148. final jsondata = json.decode(configPtr.toDartString());
  149. final data = JsonMapper.deserialize<ClashConfigEntity>(jsondata);
  150. configEntity.value = data;
  151. // malloc.free(configPtr);
  152. }
  153. Future<void> reload() async {
  154. // get configs
  155. getConfigs();
  156. getCurrentClashConfig();
  157. // proxies
  158. getProxies();
  159. updateTray();
  160. }
  161. void initDaemon() async {
  162. printInfo(info: 'init clash service');
  163. // wait for online
  164. // while (!await isRunning()) {
  165. // printInfo(info: 'waiting online status');
  166. // await Future.delayed(const Duration(milliseconds: 500));
  167. // }
  168. // get traffic
  169. Timer.periodic(const Duration(seconds: 1), (t) {
  170. final trafficPtr = clashFFI.get_traffic().cast<Utf8>();
  171. final traffic = trafficPtr.toDartString();
  172. if (kDebugMode) {
  173. debugPrint(traffic);
  174. }
  175. try {
  176. final trafficJson = jsonDecode(traffic);
  177. uploadRate.value = trafficJson['Up'].toDouble() / 1024; // KB
  178. downRate.value = trafficJson['Down'].toDouble() / 1024; // KB
  179. // fix: 只有KDE不会导致Tray自动消失
  180. // final desktop = Platform.environment['XDG_CURRENT_DESKTOP'];
  181. // updateTray();
  182. } catch (e) {
  183. Get.printError(info: '$e');
  184. }
  185. // malloc.free(trafficPtr);
  186. });
  187. // system proxy
  188. // listen port
  189. await reload();
  190. checkPort();
  191. if (isSystemProxy()) {
  192. setSystemProxy();
  193. }
  194. }
  195. @override
  196. void onClose() {
  197. closeClashDaemon();
  198. super.onClose();
  199. }
  200. Future<void> closeClashDaemon() async {
  201. Get.printInfo(info: 'fclash: closing daemon');
  202. // double check
  203. // stopClashSubP();
  204. if (isSystemProxy()) {
  205. // just clear system proxy
  206. await clearSystemProxy(permanent: false);
  207. }
  208. await _clashLock?.unlock();
  209. }
  210. Future<void> setSystemProxy() async {
  211. if (isDesktop) {
  212. if (configEntity.value != null) {
  213. final entity = configEntity.value!;
  214. if (entity.port != 0) {
  215. await Future.wait([
  216. proxyManager.setAsSystemProxy(
  217. ProxyTypes.http, '127.0.0.1', entity.port!),
  218. proxyManager.setAsSystemProxy(
  219. ProxyTypes.https, '127.0.0.1', entity.port!)
  220. ]);
  221. debugPrint("set http");
  222. }
  223. if (entity.socksPort != 0 && !Platform.isWindows) {
  224. debugPrint("set socks");
  225. await proxyManager.setAsSystemProxy(
  226. ProxyTypes.socks, '127.0.0.1', entity.socksPort!);
  227. }
  228. await setIsSystemProxy(true);
  229. }
  230. } else {
  231. if (configEntity.value != null) {
  232. final entity = configEntity.value!;
  233. if (entity.port != 0) {
  234. // await mobileChannel
  235. // .invokeMethod("SetHttpPort", {"port": entity.port});
  236. }
  237. // mobileChannel.invokeMethod("StartProxy");
  238. await setIsSystemProxy(true);
  239. }
  240. // await Clipboard.setData(
  241. // ClipboardData(text: "${configEntity.value?.port}"));
  242. // final dialog = BrnDialog(
  243. // titleText: "请手动设置代理",
  244. // messageText:
  245. // "端口号已复制。请进入已连接WiFi的详情设置,将代理设置为手动,主机名填写127.0.0.1,端口填写${configEntity.value?.port},然后返回点击已完成即可",
  246. // actionsText: ["取消", "已完成", "去设置填写"],
  247. // indexedActionCallback: (index) async {
  248. // if (index == 0) {
  249. // if (Get.isOverlaysOpen) {
  250. // Get.back();
  251. // }
  252. // } else if (index == 1) {
  253. // final proxy = await SystemProxy.getProxySettings();
  254. // if (proxy != null) {
  255. // if (proxy["host"] == "127.0.0.1" &&
  256. // int.parse(proxy["port"].toString()) ==
  257. // configEntity.value?.port) {
  258. // Future.delayed(Duration.zero, () {
  259. // if (Get.overlayContext != null) {
  260. // BrnToast.show("设置成功", Get.overlayContext!);
  261. // setIsSystemProxy(true);
  262. // }
  263. // });
  264. // if (Get.isOverlaysOpen) {
  265. // Get.back();
  266. // }
  267. // }
  268. // } else {
  269. // Future.delayed(Duration.zero, () {
  270. // if (Get.overlayContext != null) {
  271. // BrnToast.show("好像未完成设置哦", Get.overlayContext!);
  272. // }
  273. // });
  274. // }
  275. // } else {
  276. // Future.delayed(Duration.zero, () {
  277. // BrnToast.show("端口号已复制", Get.context!);
  278. // });
  279. // await OpenSettings.openWIFISetting();
  280. // }
  281. // },
  282. // );
  283. // Get.dialog(dialog);
  284. }
  285. }
  286. Future<void> clearSystemProxy({bool permanent = true}) async {
  287. if (isDesktop) {
  288. await proxyManager.cleanSystemProxy();
  289. if (permanent) {
  290. await setIsSystemProxy(false);
  291. }
  292. } else {
  293. //mobileChannel.invokeMethod("StopProxy");
  294. await setIsSystemProxy(false);
  295. // final dialog = BrnDialog(
  296. // titleText: "请手动设置代理",
  297. // messageText: "请进入已连接WiFi的详情设置,将代理设置为无",
  298. // actionsText: ["取消", "已完成", "去设置清除"],
  299. // indexedActionCallback: (index) async {
  300. // if (index == 0) {
  301. // if (Get.isOverlaysOpen) {
  302. // Get.back();
  303. // }
  304. // } else if (index == 1) {
  305. // final proxy = await SystemProxy.getProxySettings();
  306. // if (proxy != null) {
  307. // Future.delayed(Duration.zero, () {
  308. // if (Get.overlayContext != null) {
  309. // BrnToast.show("好像没有清除成功哦,当前代理${proxy}", Get.overlayContext!);
  310. // }
  311. // });
  312. // } else {
  313. // Future.delayed(Duration.zero, () {
  314. // if (Get.overlayContext != null) {
  315. // BrnToast.show("清除成功", Get.overlayContext!);
  316. // }
  317. // setIsSystemProxy(false);
  318. // if (Get.isOverlaysOpen) {
  319. // Get.back();
  320. // }
  321. // });
  322. // }
  323. // } else {
  324. // OpenSettings.openWIFISetting().then((_) async {
  325. // final proxy = await SystemProxy.getProxySettings();
  326. // debugPrint("$proxy");
  327. // });
  328. // }
  329. // },
  330. // );
  331. // Get.dialog(dialog);
  332. }
  333. }
  334. void getProxies() {
  335. final proxiesPtr = clashFFI.get_proxies().cast<Utf8>();
  336. proxies.value = json.decode(proxiesPtr.toDartString());
  337. // malloc.free(proxiesPtr);
  338. }
  339. bool isSystemProxy() {
  340. return SpUtil.getData('system_proxy', defValue: false);
  341. }
  342. Future<bool> setIsSystemProxy(bool proxy) {
  343. isSystemProxyObs.value = proxy;
  344. return SpUtil.setData('system_proxy', proxy);
  345. }
  346. void checkPort() {
  347. if (configEntity.value != null) {
  348. if (configEntity.value!.port == 0) {
  349. changeConfigField('port', initializedHttpPort);
  350. }
  351. if (configEntity.value!.mixedPort == 0) {
  352. changeConfigField('mixed-port', initializedMixedPort);
  353. }
  354. if (configEntity.value!.socksPort == 0) {
  355. changeConfigField('socks-port', initializedSockPort);
  356. }
  357. updateTray();
  358. }
  359. }
  360. //切换配置
  361. bool changeConfigField(String field, dynamic value) {
  362. try {
  363. int ret = clashFFI.change_config_field(
  364. json.encode(<String, dynamic>{field: value}).toNativeUtf8().cast());
  365. return ret == 0;
  366. } finally {
  367. getCurrentClashConfig();
  368. if (field.endsWith("port") && isSystemProxy()) {
  369. setSystemProxy();
  370. }
  371. }
  372. }
  373. Future<void> _acquireLock(Directory clashDirectory) async {
  374. final path = p.join(clashDirectory.path, "fclash.lock");
  375. final lockFile = File(path);
  376. if (!lockFile.existsSync()) {
  377. lockFile.createSync(recursive: true);
  378. }
  379. try {
  380. _clashLock = await lockFile.open(mode: FileMode.write);
  381. await _clashLock?.lock();
  382. } catch (e) {
  383. // if (!Platform.isWindows) {
  384. // await Get.find<NotificationService>()
  385. // .showNotification("Fclash", "Already running, Now exit.".tr);
  386. // }
  387. exit(0);
  388. }
  389. }
  390. ReceivePort? _logReceivePort;
  391. void startLogging() {
  392. _logReceivePort?.close();
  393. _logReceivePort = ReceivePort();
  394. logStream = _logReceivePort!.asBroadcastStream();
  395. if (kDebugMode) {
  396. logStream?.listen((event) {
  397. debugPrint("LOG: ${event}");
  398. });
  399. }
  400. final nativePort = _logReceivePort!.sendPort.nativePort;
  401. debugPrint("port: $nativePort");
  402. clashFFI.start_log(nativePort);
  403. }
  404. void stopLog() {
  405. logStream = null;
  406. clashFFI.stop_log();
  407. }
  408. void updateTray() {
  409. if (!isDesktop) {
  410. return;
  411. }
  412. final stringList = List<MenuItem>.empty(growable: true);
  413. // yaml
  414. stringList
  415. .add(MenuItem(label: "profile: ${currentYaml.value}", disabled: true));
  416. if (proxies['proxies'] != null) {
  417. Map<String, dynamic> m = proxies['proxies'];
  418. m.removeWhere((key, value) => value['type'] != "Selector");
  419. var cnt = 0;
  420. for (final k in m.keys) {
  421. if (cnt >= ClashService.MAX_ENTRIES) {
  422. stringList.add(MenuItem(label: "...", disabled: true));
  423. break;
  424. }
  425. stringList.add(
  426. MenuItem(label: "${m[k]['name']}: ${m[k]['now']}", disabled: true));
  427. cnt += 1;
  428. }
  429. }
  430. // port
  431. if (configEntity.value != null) {
  432. stringList.add(
  433. MenuItem(label: 'http: ${configEntity.value?.port}', disabled: true));
  434. stringList.add(MenuItem(
  435. label: 'socks: ${configEntity.value?.socksPort}', disabled: true));
  436. }
  437. // system proxy
  438. stringList.add(MenuItem.separator());
  439. if (!isSystemProxy()) {
  440. stringList
  441. .add(MenuItem(label: "Not system proxy yet.".tr, disabled: true));
  442. stringList.add(MenuItem(
  443. label: "Set as system proxy".tr,
  444. toolTip: "click to set fclash as system proxy".tr,
  445. key: ACTION_SET_SYSTEM_PROXY));
  446. } else {
  447. stringList.add(MenuItem(label: "System proxy now.".tr, disabled: true));
  448. stringList.add(MenuItem(
  449. label: "Unset system proxy".tr,
  450. toolTip: "click to reset system proxy",
  451. key: ACTION_UNSET_SYSTEM_PROXY));
  452. stringList.add(MenuItem.separator());
  453. }
  454. initAppTray(details: stringList, isUpdate: true);
  455. }
  456. Future<bool> _changeConfig(FileSystemEntity config) async {
  457. // check if it has `rule-set`, and try to convert it
  458. final content = await convertConfig(await File(config.path).readAsString())
  459. .catchError((e) {
  460. printError(info: e);
  461. });
  462. if (content.isNotEmpty) {
  463. await File(config.path).writeAsString(content);
  464. }
  465. // judge valid
  466. if (clashFFI.is_config_valid(config.path.toNativeUtf8().cast()) == 0) {
  467. final resp = await Request.dioClient.put('/configs',
  468. queryParameters: {"force": false}, data: {"path": config.path});
  469. Get.printInfo(info: 'config changed ret: ${resp.statusCode}');
  470. currentYaml.value = basename(config.path);
  471. SpUtil.setData('yaml', currentYaml.value);
  472. return resp.statusCode == 204;
  473. } else {
  474. Future.delayed(Duration.zero, () {
  475. Get.defaultDialog(
  476. middleText: 'not a valid config file'.tr,
  477. onConfirm: () {
  478. Get.back();
  479. });
  480. });
  481. config.delete();
  482. return false;
  483. }
  484. }
  485. //切换配置文件
  486. Future<bool> changeYaml(FileSystemEntity config) async {
  487. try {
  488. if (await config.exists()) {
  489. return await _changeConfig(config);
  490. } else {
  491. return false;
  492. }
  493. } finally {
  494. reload();
  495. }
  496. }
  497. bool changeProxy(String selectName, String proxyName) {
  498. final ret = clashFFI.change_proxy(
  499. selectName.toNativeUtf8().cast(), proxyName.toNativeUtf8().cast());
  500. if (ret == 0) {
  501. reload();
  502. }
  503. return ret == 0;
  504. }
  505. }
  506. Future<String> convertConfig(String content) async {
  507. return "";
  508. }