diff --git a/lib/common/conf/app_conf.dart b/lib/common/conf/app_conf.dart index 405af65..5677936 100644 --- a/lib/common/conf/app_conf.dart +++ b/lib/common/conf/app_conf.dart @@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/rust/frb_generated.dart'; import 'package:starcitizen_doctor/data/app_version_data.dart'; import 'package:starcitizen_doctor/global_ui_model.dart'; @@ -67,6 +68,7 @@ class AppConf { /// check Rust bridge await RustLib.init(); + await RSHttp.init(); dPrint("---- rust bridge inited -----"); await SystemHelper.initPowershellPath(); diff --git a/lib/common/conf/url_conf.dart b/lib/common/conf/url_conf.dart index 3d39e9a..f726c39 100644 --- a/lib/common/conf/url_conf.dart +++ b/lib/common/conf/url_conf.dart @@ -1,5 +1,6 @@ -import 'package:dio/dio.dart'; import 'package:starcitizen_doctor/base/ui_model.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; +import 'package:starcitizen_doctor/common/rust/http_package.dart'; class URLConf { /// HOME API @@ -7,83 +8,92 @@ class URLConf { static String rssApiHome = "https://rss.sctoolbox.sccsgo.com"; static const String xkeycApiHome = "https://sctoolbox.xkeyc.com"; - static bool isUsingFallback = false; + static bool isUrlCheckPass = false; /// URLS - static String giteaAttachmentsUrl = "$gitApiHome/SCToolBox/Release"; - static String gitlabLocalizationUrl = + static String get giteaAttachmentsUrl => "$gitApiHome/SCToolBox/Release"; + + static String get gitlabLocalizationUrl => "$gitApiHome/SCToolBox/LocalizationData"; - static String apiRepoPath = "$gitApiHome/SCToolBox/api/raw/branch/main/"; - static String gitlabApiPath = "https://$gitApiHome/api/v1/"; + static String get apiRepoPath => "$gitApiHome/SCToolBox/api/raw/branch/main/"; - static String webTranslateHomeUrl = + static String get gitlabApiPath => "https://$gitApiHome/api/v1/"; + + static String get webTranslateHomeUrl => "$gitApiHome/SCToolBox/ScWeb_Chinese_Translate/raw/branch/main/json/locales"; - static String rssVideoUrl = + static String get rssVideoUrl => "$rssApiHome/bilibili/user/channel/27976358/290653"; - static String rssTextUrl1 = "$rssApiHome/bilibili/user/article/40102960"; - static String rssTextUrl2 = + static String get rssTextUrl1 => "$rssApiHome/bilibili/user/article/40102960"; + + static String get rssTextUrl2 => "$rssApiHome/baidu/tieba/user/%E7%81%AC%E7%81%ACG%E7%81%AC%E7%81%AC&"; static const feedbackUrl = "https://txc.qq.com/products/614843"; - static const devReleaseUrl = - "https://git.sctoolbox.sccsgo.com/SCToolBox/Release/releases"; + static String get devReleaseUrl => "$gitApiHome/SCToolBox/Release/releases"; - static const _gitApiList = [ - "https://git.sctoolbox.sccsgo.com", - "https://sctb-git.xkeyc.com" - ]; - - static const _rssApiList = [ - "https://rss.sctoolbox.sccsgo.com", - "https://rss.42kit.com" - ]; - - static checkHost() async { - final dio = Dio(BaseOptions(connectTimeout: const Duration(seconds: 5))); - bool hasAvailable = false; - // 寻找可用的 git API - for (var value in _gitApiList) { - try { - final resp = await dio.head(value); - if (resp.statusCode == 200) { - dPrint("[URLConf].checkHost passed $value"); - gitApiHome = value; - hasAvailable = true; - break; - } - isUsingFallback = true; - continue; - } catch (e) { - dPrint("[URLConf].checkHost $value Error= $e"); - isUsingFallback = true; - continue; - } + static Future checkHost() async { + // 使用 DNS 获取可用列表 + final gitApiList = + _genFinalList(await RSHttp.dnsLookupTxt("git.dns.scbox.org")); + dPrint("DNS gitApiList ==== $gitApiList"); + final fasterGit = await getFasterUrl(gitApiList); + dPrint("gitApiList.Faster ==== $fasterGit"); + if (fasterGit != null) { + gitApiHome = fasterGit; } - // 寻找可用的 RSS API - for (var value in _rssApiList) { - try { - final resp = await dio.head(value); - if (resp.statusCode == 200) { - rssApiHome = value; - hasAvailable = true; - dPrint("[URLConf].checkHost passed $value"); - break; - } - isUsingFallback = true; - continue; - } catch (e) { - dPrint("[URLConf].checkHost $value Error= $e"); - isUsingFallback = true; - continue; + final rssApiList = + _genFinalList(await RSHttp.dnsLookupTxt("rss.dns.scbox.org")); + final fasterRss = await getFasterUrl(rssApiList); + dPrint("DNS rssApiList ==== $rssApiList"); + dPrint("rssApiList.Faster ==== $fasterRss"); + if (fasterRss != null) { + rssApiHome = fasterRss; + } + isUrlCheckPass = fasterGit != null && fasterRss != null; + return isUrlCheckPass; + } + + static Future getFasterUrl(List urls) async { + String firstUrl = ""; + int callLen = 0; + + void onCall(RustHttpResponse? response, String url) { + callLen++; + if (response != null && response.statusCode == 200 && firstUrl.isEmpty) { + firstUrl = url; } } - if (!hasAvailable) { - isUsingFallback = false; + for (var value in urls) { + RSHttp.head(value).then((resp) => onCall(resp, value), onError: (err) { + callLen++; + dPrint("RSHttp.head error $err"); + }); + } + + while (true) { + await Future.delayed(const Duration(milliseconds: 16)); + if (firstUrl.isNotEmpty) { + return firstUrl; + } + if (callLen == urls.length && firstUrl.isEmpty) { + return null; + } } } + + static List _genFinalList(List sList) { + List list = []; + for (var ll in sList) { + final ssList = ll.split(","); + for (var value in ssList) { + list.add(value); + } + } + return list; + } } diff --git a/lib/common/io/rs_http.dart b/lib/common/io/rs_http.dart index 0554d3b..fb987bf 100644 --- a/lib/common/io/rs_http.dart +++ b/lib/common/io/rs_http.dart @@ -1,10 +1,19 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:starcitizen_doctor/common/conf/app_conf.dart'; import 'package:starcitizen_doctor/common/rust/api/http_api.dart' as rust_http; import 'package:starcitizen_doctor/common/rust/api/http_api.dart'; +import 'package:starcitizen_doctor/common/rust/http_package.dart'; class RSHttp { + static init() async { + await rust_http.setDefaultHeader(headers: { + "User-Agent": + "SCToolBox/${AppConf.appVersion} (${AppConf.appVersionCode}) ${AppConf.isMSE ? "" : " DEV"} RSHttp" + }); + } + static Future getText(String url, {Map? headers}) async { final r = await rust_http.fetch( @@ -26,4 +35,15 @@ class RSHttp { method: MyMethod.post, url: url, headers: headers, inputData: data); return r.statusCode == 200; } + + static Future head(String url, + {Map? headers}) async { + final r = await rust_http.fetch( + method: MyMethod.head, url: url, headers: headers); + return r; + } + + static Future> dnsLookupTxt(String host) async { + return await rust_http.dnsLookupTxt(host: host); + } } diff --git a/lib/common/rust/api/http_api.dart b/lib/common/rust/api/http_api.dart index 3a4fba4..6b16989 100644 --- a/lib/common/rust/api/http_api.dart +++ b/lib/common/rust/api/http_api.dart @@ -24,6 +24,9 @@ Future fetch( inputData: inputData, hint: hint); +Future> dnsLookupTxt({required String host, dynamic hint}) => + RustLib.instance.api.dnsLookupTxt(host: host, hint: hint); + // Rust type: RustOpaqueMoi> @sealed class ReqwestVersion extends RustOpaque { diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index eef03c6..ab81fd2 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -74,6 +74,8 @@ abstract class RustLibApi extends BaseApi { required int connectionCount, dynamic hint}); + Future> dnsLookupTxt({required String host, dynamic hint}); + Future fetch( {required MyMethod method, required String url, @@ -160,6 +162,31 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["url", "savePath", "fileName", "connectionCount"], ); + @override + Future> dnsLookupTxt({required String host, dynamic hint}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(host, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 5, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_list_String, + decodeErrorData: null, + ), + constMeta: kDnsLookupTxtConstMeta, + argValues: [host], + apiImpl: this, + hint: hint, + )); + } + + TaskConstMeta get kDnsLookupTxtConstMeta => const TaskConstMeta( + debugName: "dns_lookup_txt", + argNames: ["host"], + ); + @override Future fetch( {required MyMethod method, @@ -283,6 +310,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + List dco_decode_list_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_String).toList(); + } + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -473,6 +506,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getInt32(); } + @protected + List sse_decode_list_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_String(deserializer)); + } + return ans_; + } + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -685,6 +730,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putInt32(self); } + @protected + void sse_encode_list_String(List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_String(item, serializer); + } + } + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer) { diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index 0700dc8..5355897 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -50,6 +50,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_i_32(dynamic raw); + @protected + List dco_decode_list_String(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -122,6 +125,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_i_32(SseDeserializer deserializer); + @protected + List sse_decode_list_String(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -202,6 +208,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer); diff --git a/lib/common/rust/frb_generated.web.dart b/lib/common/rust/frb_generated.web.dart index eda2260..1c9fc2c 100644 --- a/lib/common/rust/frb_generated.web.dart +++ b/lib/common/rust/frb_generated.web.dart @@ -49,6 +49,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_i_32(dynamic raw); + @protected + List dco_decode_list_String(dynamic raw); + @protected Uint8List dco_decode_list_prim_u_8_strict(dynamic raw); @@ -121,6 +124,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_i_32(SseDeserializer deserializer); + @protected + List sse_decode_list_String(SseDeserializer deserializer); + @protected Uint8List sse_decode_list_prim_u_8_strict(SseDeserializer deserializer); @@ -201,6 +207,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_list_String(List self, SseSerializer serializer); + @protected void sse_encode_list_prim_u_8_strict( Uint8List self, SseSerializer serializer); diff --git a/lib/global_ui_model.dart b/lib/global_ui_model.dart index 39a4dd0..cf0958a 100644 --- a/lib/global_ui_model.dart +++ b/lib/global_ui_model.dart @@ -35,7 +35,7 @@ class AppGlobalUIModel extends BaseUIModel { await Future.delayed(const Duration(milliseconds: 100)); if (AppConf.networkVersionData == null) { showToast(context, - "检查更新失败!请检查网络连接... \n进入离线模式.. \n\n请谨慎在离线模式中使用。\n请尝试更换无污染的DNS。 \n当前版本构建日期:${AppConf.appVersionDate}\n QQ群:940696487 \n错误信息:$checkUpdateError"); + "网络异常,这可能是服务器正在维护或遭受攻击... \n进入离线模式.. \n\n请谨慎在离线模式中使用。 \n当前版本构建日期:${AppConf.appVersionDate}\n QQ群:940696487 \n错误信息:$checkUpdateError"); return false; } final lastVersion = AppConf.isMSE diff --git a/lib/ui/home/home_ui_model.dart b/lib/ui/home/home_ui_model.dart index 852d9b1..2111766 100644 --- a/lib/ui/home/home_ui_model.dart +++ b/lib/ui/home/home_ui_model.dart @@ -133,12 +133,6 @@ class HomeUIModel extends BaseUIModel { appUpdateTimer = Timer.periodic(const Duration(minutes: 30), (timer) { _checkLocalizationUpdate(); }); - Future.delayed(const Duration(milliseconds: 100)).then((value) { - if (URLConf.isUsingFallback) { - if (!mounted) return; - showToast(context!, "因源服务器异常(机房故障或遭受攻击),当前正在使用备用线路,可能会出现访问速度下降,敬请谅解。"); - } - }); super.initModel(); } diff --git a/rust/src/api/http_api.rs b/rust/src/api/http_api.rs index 281a811..4191b09 100644 --- a/rust/src/api/http_api.rs +++ b/rust/src/api/http_api.rs @@ -39,4 +39,8 @@ pub async fn fetch(method: MyMethod, headers: Option>, input_data: Option>) -> RustHttpResponse { http_package::fetch(_my_method_to_hyper_method(method), url, headers, input_data).await.unwrap() +} + +pub async fn dns_lookup_txt(host: String) -> Vec { + http_package::dns_lookup_txt(host).await.unwrap() } \ No newline at end of file diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 4e4d894..07737a6 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -94,6 +94,41 @@ let api_connection_count = ::sse_decode(&mut deserializer);deserializer.end( })().await) } }) } +fn wire_dns_lookup_txt_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "dns_lookup_txt", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_host = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse( + (move || async move { + Result::<_, ()>::Ok(crate::api::http_api::dns_lookup_txt(api_host).await) + })() + .await, + ) + } + }, + ) +} fn wire_fetch_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -244,6 +279,18 @@ impl SseDecode for i32 { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -448,6 +495,7 @@ fn pde_ffi_dispatcher_primary_impl( match func_id { 2 => wire_cancel_download_impl(port, ptr, rust_vec_len, data_len), 1 => wire_start_download_impl(port, ptr, rust_vec_len, data_len), + 5 => wire_dns_lookup_txt_impl(port, ptr, rust_vec_len, data_len), 4 => wire_fetch_impl(port, ptr, rust_vec_len, data_len), 3 => wire_set_default_header_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), @@ -675,6 +723,16 @@ impl SseEncode for i32 { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/http_package/dns.rs b/rust/src/http_package/dns.rs index 64b582e..94c2c2d 100644 --- a/rust/src/http_package/dns.rs +++ b/rust/src/http_package/dns.rs @@ -3,8 +3,6 @@ use hickory_resolver::{lookup_ip::LookupIpIntoIter, TokioAsyncResolver}; use hyper::client::connect::dns::Name; use once_cell::sync::OnceCell; use reqwest::dns::{Addrs, Resolve, Resolving}; - -use std::io; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; @@ -35,6 +33,17 @@ impl Resolve for MyHickoryDnsResolver { } } +impl MyHickoryDnsResolver { + pub(crate) async fn lookup_txt(&self, name: String) -> anyhow::Result> { + let resolver = self.state.get_or_try_init(new_resolver)?; + let txt = resolver.txt_lookup(name).await?; + let t = txt.iter() + .map(|rdata| rdata.to_string()) + .collect::>(); + Ok(t) + } +} + impl Iterator for SocketAddrs { type Item = SocketAddr; diff --git a/rust/src/http_package/mod.rs b/rust/src/http_package/mod.rs index 543fb16..16a7e2b 100644 --- a/rust/src/http_package/mod.rs +++ b/rust/src/http_package/mod.rs @@ -21,12 +21,13 @@ pub struct RustHttpResponse { lazy_static! { static ref DEFAULT_HEADER: RwLock = RwLock::from(HeaderMap::new()); + static ref DNS_CLIENT : Arc = Arc::from(dns::MyHickoryDnsResolver::default()); static ref HTTP_CLIENT: reqwest::Client = { reqwest::Client::builder() .use_rustls_tls() .connect_timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(10)) - .dns_resolver(Arc::from(dns::MyHickoryDnsResolver::default())) + .dns_resolver(DNS_CLIENT.clone()) .build() .unwrap() }; @@ -83,6 +84,10 @@ pub async fn fetch( Ok(resp) } +pub async fn dns_lookup_txt(name: String) -> anyhow::Result> { + DNS_CLIENT.lookup_txt(name).await +} + fn _reade_resp_header(r_header: &HeaderMap) -> HashMap { let mut resp_headers = HashMap::new(); for ele in r_header {