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: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:9090'.obs; var clashCoreApiSecret = ''.obs; var clashCoreDns = ''.obs; var clashCoreTunEnable = false.obs; var servicePort = 0.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 readClashCoreApi(); } 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: www.amazon.com, 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 makeClashConfig(List nodes) async{ if( Files.makeProxyConfig.existsSync()){ Files.makeProxyConfig.deleteSync(recursive: true); } var stack = "system"; if( Platform.isWindows){ stack = "gvisor"; } var dnsPort = 1553; if(clashCoreTunEnable.value == true) { dnsPort = 53; } var mixedport = 9888; bool bg = await isPortOccupied(mixedport); if(bg) { mixedport = await getFreePort(); } var extePort = 9777; final ac = await isPortOccupied(extePort); if(ac) { extePort = await getFreePort(); } 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 allow-lan: true bind-address: '*' mode: Rule log-level: info external-controller: '127.0.0.1:$extePort' unified-delay: false geodata-mode: true tcp-concurrent: false find-process-mode: strict global-client-fingerprint: chrome dns: nameserver: - 114.114.114.114 - 119.29.29.29 - https://doh.pub/dns-query - https://dns.alidns.com/dns-query fallback: - https://dns.cloudflare.com/dns-query - "[2001:da8::666]:53" - https://public.dns.iij.jp/dns-query - https://jp.tiar.app/dns-query - https://jp.tiarap.org/dns-query - tls://dot.tiar.app enable: true ipv6: false # enhanced-mode: redir-host enhanced-mode: fake-ip fake-ip-range: 198.18.0.1/16 listen: 0.0.0.0:$dnsPort fake-ip-filter: - "*.lan" default-nameserver: - 114.114.114.114 - 119.29.29.29 - "[2001:da8::666]:53" 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(); // 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'''(? 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 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; } }