使用 DNS 分流

This commit is contained in:
xkeyC 2024-02-07 22:19:43 +08:00
parent 95b4b8b947
commit 9ee02e9312
13 changed files with 247 additions and 70 deletions

View File

@ -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();

View File

@ -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 Future<bool> 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;
}
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 const _rssApiList = [
"https://rss.sctoolbox.sccsgo.com",
"https://rss.42kit.com"
];
static Future<String?> getFasterUrl(List<String> urls) async {
String firstUrl = "";
int callLen = 0;
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;
}
}
// 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;
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<String> _genFinalList(List<String> sList) {
List<String> list = [];
for (var ll in sList) {
final ssList = ll.split(",");
for (var value in ssList) {
list.add(value);
}
}
return list;
}
}

View File

@ -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<String> getText(String url,
{Map<String, String>? 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<RustHttpResponse> head(String url,
{Map<String, String>? headers}) async {
final r = await rust_http.fetch(
method: MyMethod.head, url: url, headers: headers);
return r;
}
static Future<List<String>> dnsLookupTxt(String host) async {
return await rust_http.dnsLookupTxt(host: host);
}
}

View File

@ -24,6 +24,9 @@ Future<RustHttpResponse> fetch(
inputData: inputData,
hint: hint);
Future<List<String>> dnsLookupTxt({required String host, dynamic hint}) =>
RustLib.instance.api.dnsLookupTxt(host: host, hint: hint);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::rust_async::RwLock<reqwest :: Version>>
@sealed
class ReqwestVersion extends RustOpaque {

View File

@ -74,6 +74,8 @@ abstract class RustLibApi extends BaseApi {
required int connectionCount,
dynamic hint});
Future<List<String>> dnsLookupTxt({required String host, dynamic hint});
Future<RustHttpResponse> fetch(
{required MyMethod method,
required String url,
@ -160,6 +162,31 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["url", "savePath", "fileName", "connectionCount"],
);
@override
Future<List<String>> 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<RustHttpResponse> fetch(
{required MyMethod method,
@ -283,6 +310,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as int;
}
@protected
List<String> dco_decode_list_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return (raw as List<dynamic>).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<String> 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_ = <String>[];
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<String> 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) {

View File

@ -50,6 +50,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
int dco_decode_i_32(dynamic raw);
@protected
List<String> 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<RustLibWire> {
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
List<String> 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<RustLibWire> {
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self, SseSerializer serializer);

View File

@ -49,6 +49,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
int dco_decode_i_32(dynamic raw);
@protected
List<String> 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<RustLibWire> {
@protected
int sse_decode_i_32(SseDeserializer deserializer);
@protected
List<String> 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<RustLibWire> {
@protected
void sse_encode_i_32(int self, SseSerializer serializer);
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_strict(
Uint8List self, SseSerializer serializer);

View File

@ -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

View File

@ -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();
}

View File

@ -40,3 +40,7 @@ pub async fn fetch(method: MyMethod,
input_data: Option<Vec<u8>>) -> 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<String> {
http_package::dns_lookup_txt(host).await.unwrap()
}

View File

@ -94,6 +94,41 @@ let api_connection_count = <u8>::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::SseCodec, _, _, _>(
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 = <String>::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<String> {
// 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_ = <i32>::sse_decode(deserializer);
let mut ans_ = vec![];
for idx_ in 0..len_ {
ans_.push(<String>::sse_decode(deserializer));
}
return ans_;
}
}
impl SseDecode for Vec<u8> {
// 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<String> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(self.len() as _, serializer);
for item in self {
<String>::sse_encode(item, serializer);
}
}
}
impl SseEncode for Vec<u8> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {

View File

@ -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<Vec<String>> {
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::<Vec<_>>();
Ok(t)
}
}
impl Iterator for SocketAddrs {
type Item = SocketAddr;

View File

@ -21,12 +21,13 @@ pub struct RustHttpResponse {
lazy_static! {
static ref DEFAULT_HEADER: RwLock<HeaderMap> = RwLock::from(HeaderMap::new());
static ref DNS_CLIENT : Arc<dns::MyHickoryDnsResolver> = 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<Vec<String>> {
DNS_CLIENT.lookup_txt(name).await
}
fn _reade_resp_header(r_header: &HeaderMap) -> HashMap<String, String> {
let mut resp_headers = HashMap::new();
for ele in r_header {