import 'dart:io'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:naiyouwl/app/bean/config.dart'; import 'package:naiyouwl/app/const/const.dart'; import 'package:naiyouwl/app/controller/controllers.dart'; import 'package:yaml/yaml.dart'; import 'package:path/path.dart' as path; import 'package:flutter_emoji/flutter_emoji.dart'; import '../data/model/NodeMode.dart'; final Map _defaultConfig = { 'selected': 'example.yaml', 'updateInterval': 86400, 'updateSubsAtStart': false, 'setSystemProxy': false, 'startAtLogin': false, 'breakConnections': false, 'language': 'zh_CN', 'port': 9899, 'subs': [], }; class ConfigController extends GetxController { late final dio; var config = Config.fromJson(_defaultConfig).obs; var clashCoreApiAddress = '127.0.0.1:9799'.obs; var clashCoreApiSecret = ''.obs; var clashCoreDns = ''.obs; var clashCoreTunEnable = false.obs; var servicePort = 9899.obs; var mixedPort = 9788.obs; var ApiAddressPort = 9799.obs; Future initConfig() async { // var port = await getFreePort(); //dio.addSentry(); dio = Dio(BaseOptions(baseUrl: clashCoreApiAddress.value)); if (!await Paths.config.exists()) await Paths.config.create(recursive: true); if (!await Files.configCountryMmdb.exists()) await Files.assetsCountryMmdb.copy(Files.configCountryMmdb.path); if (!await Files.configGeoIP.exists()) await Files.assetsGeoIP.copy(Files.configGeoIP.path); if (!await Files.configGeosite.exists()) await Files.assetsGeosite.copy(Files.configGeosite.path); if (Platform.isWindows && !await Files.configWintun.exists()) await Files.assetsWintun.copy(Files.configWintun.path); final locale = Get.deviceLocale!; _defaultConfig['language'] = '${locale.languageCode}_${locale.countryCode}'; if (await Files.configConfig.exists()) { final local = json.decode(await Files.configConfig.readAsString()); config.value = Config.fromJson({..._defaultConfig, ...local}); } else { config.value = Config.fromJson(_defaultConfig); } // bool bg = await isPortOccupied(config.value.servicePort); // if(bg) { // config.value.servicePort = await getFreePort(); // } if (config.value.subs.isEmpty) { if (!await Files.configExample.exists()) { await Files.assetsExample.copy(Files.configExample.path); } config.value.subs.add(ConfigSub(name: 'example.yaml', url: '', updateTime: 0)); config.value.selected = 'example.yaml'; } await save(); await makeInitConfig(); } String nodeToYaml(NodeMode node) { switch (node.type) { case 'trojan': return ''' - { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, udp: 1 }'''; case 'shadowsocks': return ''' - { name: ${node.name}, type: ss, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, cipher: ${node.method}, udp: 1 }'''; case 'v2ray': final type = (node.vless == 1) ? 'vless' : 'vmess'; if (type == 'vless') { return ''' - { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, udp: 1, flow: xtls-rprx-vision, servername: ${node.v2Sni}, tls: true, reality-opts: { public-key: ${node.vlessPulkey} } }'''; } else { return ''' - { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, cipher: ${node.method}, udp: 1 }'''; } default: return ''; } } // Future makeInitConfig() async{ var mode = controllers.global.modesSelect; var initconfig = ''' mixed-port: ${mixedPort.value} allow-lan: true bind-address: '*' mode: $mode log-level: info external-controller: '127.0.0.1:${ApiAddressPort.value}' unified-delay: false geodata-mode: true tcp-concurrent: false find-process-mode: strict global-client-fingerprint: chrome proxies: rules: - GEOIP,CN,DIRECT - MATCH,DIRECT '''; await Files.makeInitProxyConfig.writeAsString(initconfig); config.value.selected = 'init_proxy.yaml'; await readClashCoreApi(); } Future makeClashConfig(List nodes) async{ // if( Files.makeProxyConfig.existsSync()){ // Files.makeProxyConfig.deleteSync(recursive: true); // } //await portDetection(); var stack = "system"; if( Platform.isWindows){ stack = "gvisor"; } var dnsPort = 53; if(clashCoreTunEnable.value == true) { dnsPort = 53; } var dnsEnab = true; if(clashCoreTunEnable.value == true){ dnsEnab = true; } var mode = controllers.global.modesSelect; var proxies = nodes.map(nodeToYaml).toList(); var proxyGroups = ''' proxy-groups: - name: proxy type: select proxies: - ${nodes.map((node) => node.name).join('\n - ')} '''; var rules = ''' rules: - GEOIP,CN,DIRECT - MATCH,proxy '''; // var initconfig = ''' mixed-port: ${mixedPort.value} allow-lan: true bind-address: '*' mode: $mode log-level: info external-controller: '127.0.0.1:${ApiAddressPort.value}' unified-delay: false geodata-mode: true tcp-concurrent: false find-process-mode: strict global-client-fingerprint: chrome dns: enable: $dnsEnab listen: :$dnsPort ipv6: false enhanced-mode: redir-host # fake-ip-range: 28.0.0.1/8 # fake-ip-filter: # - '*' # - '+.lan' default-nameserver: - 119.29.29.29 - 223.5.5.5 nameserver: - 'tls://8.8.4.4#proxy' - 'https://1.0.0.1/dns-query#proxy' proxy-server-nameserver: - tls://223.5.5.5 nameserver-policy: "geosite:cn": [tls://119.29.29.29] tun: enable: ${clashCoreTunEnable.value} stack: $stack # stack: gvisor dns-hijack: - 198.18.0.2:53 # when `fake-ip-range` is 198.18.0.1/16, should hijack 198.18.0.2:53 auto-route: true # auto set global route for Windows # It is recommended to use `interface-name` auto-detect-interface: true # auto detect interface, conflict with `interface-name` proxies: ${proxies.join('\n')} $proxyGroups $rules '''; await Files.makeProxyConfig.writeAsString(initconfig); config.value.selected = 'proxy.yaml'; await readClashCoreApi(); } Future save() async { await Files.configConfig.writeAsString(json.encode(config.toJson())); } Future readClashCoreApi() async { final configStr = await File(path.join(Paths.config.path, config.value.selected)).readAsString(); //print("config ---- $configStr"); // final emoji = EmojiParser(); // final b = emoji.unemojify(_config); final configJson = loadYaml(configStr.replaceAll(EmojiParser.REGEX_EMOJI, 'emoji')); // print(_json["external-controller"]); // https://github.com/dart-lang/yaml/issues/53 // final _extControl = RegExp(r'''(? setSerivcePort(int port) async { config.value.servicePort = port; await save(); config.refresh(); } Future setLanguage(String language) async { config.value.language = language; await save(); config.refresh(); } Future setSystemProxy(bool open) async { config.value.setSystemProxy = open; await save(); config.refresh(); } Future setUpdateInterval(int value) async { config.value.updateInterval = value; await save(); config.refresh(); } Future setUpdateSubsAtStart(bool value) async { config.value.updateSubsAtStart = value; await save(); config.refresh(); } Future setSelectd(String selected) async { config.value.selected = selected; await save(); config.refresh(); } Future updateSub(ConfigSub sub) async { if ((sub.url ?? '').isEmpty) return false; final res = await dio.get(sub.url!); final subInfo = res.headers['subscription-userinfo']; final file = File(path.join(Paths.config.path, sub.name)); final oldConfig = await file.exists() ? await file.readAsString() : ''; final changed = oldConfig != res.data; if (changed) await file.writeAsString(res.data); sub.updateTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; sub.info = null; if (subInfo != null) { // final info = Map.fromEntries( // subInfo.first.split(RegExp(r';\s*')).where((s) => s.isNotEmpty).map((e) => e.split('=')).map((e) => MapEntry(e[0], int.parse(e[1])))); final entries = subInfo.first.split(RegExp(r';\s*')) .where((s) => s.isNotEmpty) .map((e) => e.split('=')) .toList(); final info = {}; for (var entry in entries) { info[entry[0]] = int.parse(entry[1]); } sub.info = ConfigSubInfo.fromJson(info); } await setSub(sub.name, sub); return changed; } Future setSub(String subName, ConfigSub sub) async { final idx = config.value.subs.indexWhere((it) => it.name == subName); config.value.subs[idx] = sub; if (subName != sub.name) { final file = File(path.join(Paths.config.path, subName)); if (await file.exists()) await file.rename(path.join(Paths.config.path, sub.name)); } await save(); config.refresh(); } Future addSub(ConfigSub sub) async { config.value.subs.add(sub); final file = File(path.join(Paths.config.path, sub.name)); if (!await file.exists()) await file.create(); await save(); config.refresh(); } Future deleteSub(String subName) async { final file = File(path.join(Paths.config.path, subName)); if (await file.exists()) await file.delete(); config.value.subs.removeWhere((it) => it.name == subName); await save(); config.refresh(); } Future setBreakConnections(bool value) async { config.value.breakConnections = value; await save(); config.refresh(); } Future portDetection() async { bool isOk = await isPortOccupied(mixedPort.value); if(isOk){ mixedPort.value = await getFreePort(); } //await Future.delayed(const Duration(seconds: 5)); // 等待5秒 isOk = await isPortOccupied(ApiAddressPort.value); if(isOk){ ApiAddressPort.value = await getFreePort(); } //await Future.delayed(const Duration(seconds: 5)); // 等待5秒 if(!controllers.service.isRunning){ isOk = await isPortOccupied(servicePort.value); if(isOk){ servicePort.value = await getFreePort(); } } } Future getFreePort() async { var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); int port = server.port; await server.close(); return port; } Future isPortOccupied(int port) async { bool isOccupied = false; ServerSocket? server; try { server = await ServerSocket.bind(InternetAddress.loopbackIPv4, port); } catch (e) { isOccupied = true; } finally { await server?.close(); } return isOccupied; } }