589 lines
19 KiB
Dart
Executable File
589 lines
19 KiB
Dart
Executable File
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:dio/io.dart';
|
||
|
||
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
|
||
import 'package:flutter_loggy_dio/flutter_loggy_dio.dart';
|
||
|
||
import 'package:kaer_with_panels/app/common/app_config.dart';
|
||
import 'package:kaer_with_panels/app/common/app_run_data.dart';
|
||
import 'package:kaer_with_panels/app/network/base_response.dart';
|
||
import 'package:kaer_with_panels/app/localization/kr_language_utils.dart';
|
||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||
import 'package:kaer_with_panels/app/services/singbox_imp/kr_sing_box_imp.dart';
|
||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||
import 'package:kaer_with_panels/singbox/model/singbox_status.dart';
|
||
|
||
// import 'package:crypto/crypto.dart';
|
||
// import 'package:encrypt/encrypt.dart';
|
||
|
||
import 'package:loggy/loggy.dart';
|
||
|
||
import '../utils/kr_aes_util.dart';
|
||
import '../utils/kr_log_util.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
|
||
// import 'package:video/app/utils/common_util.dart';
|
||
// import 'package:video/app/utils/log_util.dart';
|
||
|
||
/// 定义请求方法的枚举
|
||
enum HttpMethod { GET, POST, DELETE, PUT }
|
||
|
||
/// 封装请求
|
||
class HttpUtil {
|
||
final Dio _dio = Dio();
|
||
static final HttpUtil _instance = HttpUtil._internal();
|
||
|
||
HttpUtil._internal() {
|
||
initDio();
|
||
}
|
||
|
||
factory HttpUtil() => _instance;
|
||
|
||
static HttpUtil getInstance() {
|
||
return _instance;
|
||
}
|
||
|
||
/// 对dio进行配置
|
||
void initDio() {
|
||
KRLogUtil.kr_i('🚀 HttpUtil.initDio() 开始初始化', tag: 'HttpUtil');
|
||
// 不使用 Loggy,改用自定义简洁拦截器
|
||
_dio.interceptors.add(_KRSimpleHttpInterceptor(_dio));
|
||
_dio.options.baseUrl = AppConfig.getInstance().baseUrl;
|
||
|
||
// 设置连接超时时间
|
||
_dio.options.connectTimeout = const Duration(seconds: 60);
|
||
_dio.options.receiveTimeout = const Duration(seconds: 60);
|
||
_dio.options.sendTimeout = const Duration(seconds: 60);
|
||
|
||
// 设置请求头
|
||
_dio.options.headers = {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json; charset=UTF-8',
|
||
// 移除固定的UserAgent,使用动态的
|
||
};
|
||
|
||
// 设置响应类型
|
||
_dio.options.responseType = ResponseType.json;
|
||
|
||
// 设置验证状态
|
||
_dio.options.validateStatus = (status) {
|
||
return status != null && status >= 200 && status < 500;
|
||
};
|
||
|
||
// 🔧 配置HttpClientAdapter 优先走本地 sing-box mixed 端口,
|
||
// 若代理不可用则回退到直连
|
||
KRLogUtil.kr_i('🔧 配置 HttpClientAdapter...', tag: 'HttpUtil');
|
||
_dio.httpClientAdapter = IOHttpClientAdapter(
|
||
createHttpClient: () {
|
||
KRLogUtil.kr_i('📱 createHttpClient 回调被调用', tag: 'HttpUtil');
|
||
final client = HttpClient();
|
||
|
||
// ✅ 优化:智能代理回退逻辑
|
||
client.findProxy = (url) {
|
||
try {
|
||
// 检查 SingBox 是否正在运行
|
||
final singBoxStatus = KRSingBoxImp.instance.kr_status;
|
||
final isProxyAvailable = singBoxStatus == SingboxStatus.started();
|
||
|
||
if (!isProxyAvailable) {
|
||
// 代理未运行,直接使用直连
|
||
KRLogUtil.kr_i(
|
||
'🔄 代理未运行,使用直连模式: $url',
|
||
tag: 'HttpUtil',
|
||
);
|
||
return 'DIRECT';
|
||
}
|
||
|
||
// 代理正在运行,使用代理配置
|
||
final proxyConfig = KRSingBoxImp.instance.kr_buildProxyRule();
|
||
KRLogUtil.kr_i(
|
||
'✅ 使用代理模式, url: $url, proxy: $proxyConfig',
|
||
tag: 'HttpUtil',
|
||
);
|
||
return proxyConfig;
|
||
} catch (e) {
|
||
// 发生异常时回退到直连
|
||
KRLogUtil.kr_w(
|
||
'⚠️ 代理配置异常,回退到直连: $e',
|
||
tag: 'HttpUtil',
|
||
);
|
||
return 'DIRECT';
|
||
}
|
||
};
|
||
|
||
// ✅ 优化:设置连接失败时的自动回退
|
||
client.connectionTimeout = const Duration(seconds: 10);
|
||
client.badCertificateCallback = (cert, host, port) => true;
|
||
|
||
return client;
|
||
},
|
||
);
|
||
KRLogUtil.kr_i('✅ HttpUtil.initDio() 初始化完成', tag: 'HttpUtil');
|
||
}
|
||
|
||
/// 更新baseUrl
|
||
void updateBaseUrl() {
|
||
String newBaseUrl = AppConfig.getInstance().baseUrl;
|
||
if (_dio.options.baseUrl != newBaseUrl) {
|
||
KRLogUtil.kr_i('🔄 更新baseUrl: ${_dio.options.baseUrl} -> $newBaseUrl',
|
||
tag: 'HttpUtil');
|
||
_dio.options.baseUrl = newBaseUrl;
|
||
}
|
||
}
|
||
|
||
/// 初始化请求头 :signature签名字符串
|
||
Map<String, dynamic> _initHeader(
|
||
String signature, String? userId, String? token) {
|
||
Map<String, dynamic> map = <String, dynamic>{};
|
||
|
||
if (KRAppRunData().kr_isLogin.value == true) {
|
||
map["Authorization"] = KRAppRunData().kr_token;
|
||
}
|
||
|
||
// 添加语言请求头
|
||
map["lang"] = KRLanguageUtils.getCurrentLanguageCode();
|
||
map['Login-Type'] = 'device';
|
||
|
||
// 添加动态UserAgent头
|
||
map["User-Agent"] = _kr_getUserAgent();
|
||
|
||
return map;
|
||
}
|
||
|
||
/// 获取当前系统的 user_agent
|
||
String _kr_getUserAgent() {
|
||
if (Platform.isAndroid) {
|
||
return 'android';
|
||
} else if (Platform.isIOS) {
|
||
return 'ios';
|
||
} else if (Platform.isMacOS) {
|
||
return 'mac';
|
||
} else if (Platform.isWindows) {
|
||
return 'windows';
|
||
} else if (Platform.isLinux) {
|
||
return 'linux';
|
||
} else if (Platform.isFuchsia) {
|
||
return 'harmony';
|
||
} else {
|
||
return 'unknown';
|
||
}
|
||
}
|
||
|
||
/// request请求:T为转换的实体类, path:请求地址,query:请求参数, method: 请求方法, isShowLoading(可选): 是否显示加载中的状态,默认true显示, false为不显示
|
||
Future<BaseResponse<T>> request<T>(String path, Map<String, dynamic> params,
|
||
{HttpMethod method = HttpMethod.POST, bool isShowLoading = true}) async {
|
||
try {
|
||
// 每次请求前更新baseUrl,确保使用最新的域名
|
||
updateBaseUrl();
|
||
|
||
if (isShowLoading) {
|
||
KRCommonUtil.kr_showLoading();
|
||
}
|
||
|
||
var map = <String, dynamic>{};
|
||
// 判断是否需要加密:根据站点配置的 enable_security 字段
|
||
final shouldEncrypt = KRSiteConfigService().isDeviceSecurityEnabled();
|
||
if (shouldEncrypt) {
|
||
KRLogUtil.kr_i('🔐 需要加密请求数据', tag: 'HttpUtil');
|
||
final plainText = jsonEncode(params);
|
||
map = KRAesUtil.encryptData(plainText, AppConfig.kr_encryptionKey);
|
||
} else {
|
||
map = params;
|
||
}
|
||
|
||
// 初始化请求头
|
||
final headers = _initHeader('signature', 'userId', 'token');
|
||
int? _kr_parseUserIdFromToken(String token) {
|
||
try {
|
||
// JWT格式: header.payload.signature
|
||
final parts = token.split('.');
|
||
if (parts.length != 3) {
|
||
KRLogUtil.kr_e('JWT token格式错误', tag: 'AppRunData');
|
||
return null;
|
||
}
|
||
|
||
// 解码payload部分(base64)
|
||
String payload = parts[1];
|
||
// 手动添加必要的padding(base64要求长度是4的倍数)
|
||
switch (payload.length % 4) {
|
||
case 0:
|
||
break; // 不需要padding
|
||
case 2:
|
||
payload += '==';
|
||
break;
|
||
case 3:
|
||
payload += '=';
|
||
break;
|
||
default:
|
||
KRLogUtil.kr_e('JWT payload长度无效', tag: 'AppRunData');
|
||
return null;
|
||
}
|
||
|
||
final decodedBytes = base64.decode(payload);
|
||
final decodedString = utf8.decode(decodedBytes);
|
||
|
||
// 解析JSON
|
||
final Map<String, dynamic> payloadMap = jsonDecode(decodedString);
|
||
|
||
// 获取UserId
|
||
if (payloadMap.containsKey('UserId')) {
|
||
final userId = payloadMap['UserId'];
|
||
KRLogUtil.kr_i('从JWT解析出userId: $userId', tag: 'AppRunData');
|
||
return userId is int ? userId : int.tryParse(userId.toString());
|
||
}
|
||
|
||
return null;
|
||
} catch (e) {
|
||
KRLogUtil.kr_e('解析JWT token失败: $e', tag: 'AppRunData');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
final userId =
|
||
_kr_parseUserIdFromToken(KRAppRunData().kr_token.toString());
|
||
// 调试:打印请求头
|
||
KRLogUtil.kr_i('🔍 请求头: $headers', tag: 'HttpUtil');
|
||
KRLogUtil.kr_i('🔍 请求userId: $userId', tag: 'HttpUtil');
|
||
KRLogUtil.kr_i('🔍 请求头map: $map', tag: 'HttpUtil');
|
||
|
||
Response<Map<String, dynamic>> responseTemp;
|
||
if (method == HttpMethod.GET) {
|
||
responseTemp = await _dio.get<Map<String, dynamic>>(
|
||
path,
|
||
queryParameters: map,
|
||
options: Options(
|
||
contentType: "application/json",
|
||
headers: headers, // 添加请求头
|
||
),
|
||
);
|
||
} else if (method == HttpMethod.DELETE) {
|
||
responseTemp = await _dio.delete<Map<String, dynamic>>(
|
||
path,
|
||
data: map,
|
||
options: Options(
|
||
contentType: "application/json",
|
||
headers: headers, // 添加请求头
|
||
),
|
||
);
|
||
} else if (method == HttpMethod.PUT) {
|
||
responseTemp = await _dio.put<Map<String, dynamic>>(
|
||
path,
|
||
data: map,
|
||
options: Options(
|
||
contentType: "application/json",
|
||
headers: headers, // 添加请求头
|
||
),
|
||
);
|
||
} else {
|
||
responseTemp = await _dio.post<Map<String, dynamic>>(
|
||
path,
|
||
data: map,
|
||
options: Options(
|
||
contentType: "application/json",
|
||
headers: headers, // 添加请求头
|
||
),
|
||
);
|
||
}
|
||
|
||
if (isShowLoading) {
|
||
KRCommonUtil.kr_hideLoading();
|
||
}
|
||
|
||
return BaseResponse<T>.fromJson(responseTemp.data!);
|
||
} on DioException catch (err) {
|
||
if (isShowLoading) {
|
||
KRCommonUtil.kr_hideLoading();
|
||
}
|
||
|
||
int code = -90000;
|
||
String msg = "";
|
||
msg = err.message ?? err.type.toString();
|
||
switch (err.type) {
|
||
case DioExceptionType.connectionTimeout:
|
||
code = -90001;
|
||
break;
|
||
case DioExceptionType.sendTimeout:
|
||
code = -90002;
|
||
break;
|
||
case DioExceptionType.receiveTimeout:
|
||
code = -90003;
|
||
break;
|
||
case DioExceptionType.badResponse:
|
||
code = err.response?.statusCode ?? -90004;
|
||
break;
|
||
case DioExceptionType.cancel:
|
||
break;
|
||
case DioExceptionType.connectionError:
|
||
code = -90006;
|
||
break;
|
||
case DioExceptionType.badCertificate:
|
||
code = -90007;
|
||
break;
|
||
default:
|
||
if (err.error != null) {
|
||
if (err.error.toString().contains("Connection reset by peer")) {
|
||
code = -90008;
|
||
}
|
||
}
|
||
}
|
||
print('err.type ${err.type}');
|
||
if (err.type == DioExceptionType.unknown) {
|
||
final _pathOnly = (err.requestOptions.path.isNotEmpty)
|
||
? err.requestOptions.path
|
||
: err.requestOptions.uri.path;
|
||
msg = '${msg.isNotEmpty ? msg : 'unknown'} ($_pathOnly)';
|
||
final _ua =
|
||
(err.requestOptions.extra['__unknown_attempts'] as int?) ?? 0;
|
||
if (_ua >= 2) {
|
||
KRCommonUtil.kr_showToast('请求失败($_pathOnly)', timeout: 3500);
|
||
}
|
||
}
|
||
return BaseResponse<T>.fromJson(
|
||
{'code': code, 'msg': msg, 'data': <String, dynamic>{}});
|
||
} catch (e) {
|
||
if (isShowLoading) {
|
||
KRCommonUtil.kr_hideLoading();
|
||
}
|
||
return BaseResponse<T>.fromJson({
|
||
'code': -90000,
|
||
'msg': '${e.toString()} (${path})',
|
||
'data': <String, dynamic>{}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 拦截器(简洁格式,无边框)
|
||
class MyInterceptor extends Interceptor {
|
||
@override
|
||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||
if (kDebugMode) {
|
||
print('>>> Request │ ${options.method} │ ${options.uri}');
|
||
}
|
||
if (options.data != null) {
|
||
if (kDebugMode) {
|
||
print('Body: ${options.data}');
|
||
}
|
||
}
|
||
handler.next(options);
|
||
}
|
||
|
||
@override
|
||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||
if (kDebugMode) {
|
||
print(
|
||
'<<< Response │ ${response.requestOptions.method} │ ${response.statusCode} ${response.statusMessage} │ ${response.requestOptions.uri}');
|
||
}
|
||
if (response.data != null) {
|
||
if (kDebugMode) {
|
||
print('Body: ${response.data}');
|
||
}
|
||
}
|
||
handler.next(response);
|
||
}
|
||
|
||
@override
|
||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||
if (kDebugMode) {
|
||
print(
|
||
'<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||
}
|
||
if (kDebugMode) {
|
||
print('Error Type: ${err.type}');
|
||
}
|
||
if (err.message != null) {
|
||
if (kDebugMode) {
|
||
print('Error Message: ${err.message}');
|
||
}
|
||
}
|
||
if (err.response?.data != null) {
|
||
if (kDebugMode) {
|
||
print('Response Data: ${err.response?.data}');
|
||
}
|
||
}
|
||
handler.next(err);
|
||
}
|
||
}
|
||
|
||
/// 自定义简洁 HTTP 拦截器(无边框符号)
|
||
class _KRSimpleHttpInterceptor extends Interceptor {
|
||
final Dio _dio;
|
||
_KRSimpleHttpInterceptor(this._dio);
|
||
static String? _lastPath;
|
||
static int _lastTsMs = 0;
|
||
@override
|
||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||
if (kDebugMode) {
|
||
print('>>> Request │ ${options.method} │ ${options.uri}');
|
||
}
|
||
if (options.data != null) {
|
||
if (kDebugMode) {
|
||
print('Body: ${options.data}');
|
||
}
|
||
|
||
// 检查是否是加密数据(包含 data 和 time 字段)
|
||
if (options.data is Map<String, dynamic>) {
|
||
final data = options.data as Map<String, dynamic>;
|
||
if (data.containsKey('data') && data.containsKey('time')) {
|
||
try {
|
||
if (kDebugMode) {
|
||
print('');
|
||
}
|
||
// 尝试解密并打印原始数据
|
||
final encryptedData = data['data'] as String;
|
||
final nonce = data['time'] as String;
|
||
final decrypted = KRAesUtil.decryptData(
|
||
encryptedData,
|
||
nonce,
|
||
AppConfig.kr_encryptionKey,
|
||
);
|
||
if (kDebugMode) {
|
||
print('🔓 解密后的原始请求数据:');
|
||
}
|
||
// 尝试格式化 JSON
|
||
try {
|
||
final jsonData = jsonDecode(decrypted);
|
||
final prettyJson = JsonEncoder.withIndent(' ').convert(jsonData);
|
||
if (kDebugMode) {
|
||
print(prettyJson);
|
||
}
|
||
} catch (_) {
|
||
if (kDebugMode) {
|
||
print(decrypted);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (kDebugMode) {
|
||
print('⚠️ 请求解密失败: $e');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
handler.next(options);
|
||
}
|
||
|
||
@override
|
||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||
if (kDebugMode) {
|
||
print(
|
||
'<<< Response │ ${response.requestOptions.method} │ ${response.statusCode} ${response.statusMessage} │ ${response.requestOptions.uri}');
|
||
}
|
||
if (response.data != null) {
|
||
if (kDebugMode) {
|
||
print('Body: ${response.data}');
|
||
}
|
||
|
||
// 检查响应是否是加密数据(包含 data 和 time 字段)
|
||
if (response.data is Map<String, dynamic>) {
|
||
final dataMap = response.data as Map<String, dynamic>;
|
||
|
||
// 检查是否包含嵌套的 data 字段(加密数据格式)
|
||
final nestedData = dataMap['data'];
|
||
if (nestedData is Map<String, dynamic> &&
|
||
nestedData.containsKey('data') &&
|
||
nestedData.containsKey('time')) {
|
||
try {
|
||
if (kDebugMode) {
|
||
print('');
|
||
}
|
||
if (kDebugMode) {
|
||
print('🔐 检测到加密响应,正在解密...');
|
||
}
|
||
// 尝试解密并打印原始响应数据
|
||
final encryptedData = nestedData['data'] as String;
|
||
final nonce = nestedData['time'] as String;
|
||
final decrypted = KRAesUtil.decryptData(
|
||
encryptedData,
|
||
nonce,
|
||
AppConfig.kr_encryptionKey,
|
||
);
|
||
if (kDebugMode) {
|
||
print('🔓 解密后的原始响应数据:');
|
||
}
|
||
// 尝试格式化 JSON
|
||
try {
|
||
// final jsonData = jsonDecode(decrypted);
|
||
// final prettyJson = JsonEncoder.withIndent(' ').convert(jsonData);
|
||
// if (kDebugMode) {
|
||
// print(prettyJson);
|
||
// }
|
||
} catch (_) {
|
||
if (kDebugMode) {
|
||
print(decrypted);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
if (kDebugMode) {
|
||
print('⚠️ 响应解密失败: $e');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
handler.next(response);
|
||
}
|
||
|
||
@override
|
||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||
if (kDebugMode) {
|
||
print(
|
||
'<<< Error │ ${err.requestOptions.method} │ ${err.requestOptions.uri}');
|
||
}
|
||
if (kDebugMode) {
|
||
print('Error Type: ${err.type}');
|
||
}
|
||
if (err.message != null) {
|
||
if (kDebugMode) {
|
||
print('Error Message: ${err.message}');
|
||
}
|
||
}
|
||
if (err.response?.data != null) {
|
||
if (kDebugMode) {
|
||
print('Response Data: ${err.response?.data}');
|
||
}
|
||
}
|
||
if (err.type == DioExceptionType.unknown) {
|
||
final path = (err.requestOptions.path.isNotEmpty)
|
||
? err.requestOptions.path
|
||
: err.requestOptions.uri.path;
|
||
int unknownAttempts =
|
||
(err.requestOptions.extra['__unknown_attempts'] as int?) ?? 0;
|
||
if (unknownAttempts < 2) {
|
||
err.requestOptions.extra['__unknown_attempts'] = unknownAttempts + 1;
|
||
final delayMs = unknownAttempts == 0 ? 300 : 700;
|
||
Future.delayed(Duration(milliseconds: delayMs)).then((_) async {
|
||
final start = DateTime.now();
|
||
while (!KRSingBoxImp.instance.kr_isProxyReady &&
|
||
DateTime.now().difference(start) < const Duration(seconds: 1)) {
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
}
|
||
try {
|
||
final Response<dynamic> r =
|
||
await _dio.fetch<dynamic>(err.requestOptions);
|
||
handler.resolve(r);
|
||
} catch (e) {
|
||
handler.next(e is DioException
|
||
? e
|
||
: DioException(requestOptions: err.requestOptions, error: e));
|
||
}
|
||
});
|
||
return;
|
||
} else {
|
||
final now = DateTime.now().millisecondsSinceEpoch;
|
||
if (!(_lastPath == path && (now - _lastTsMs) < 2000)) {
|
||
_lastPath = path;
|
||
_lastTsMs = now;
|
||
KRCommonUtil.kr_showToast('请求失败($path)', timeout: 3500);
|
||
}
|
||
KRLogUtil.kr_e('请求失败($path)',
|
||
tag: 'HttpUtil', error: err, stackTrace: err.stackTrace);
|
||
}
|
||
}
|
||
handler.next(err);
|
||
}
|
||
}
|