196 lines
5.9 KiB
Dart
196 lines
5.9 KiB
Dart
import 'dart:convert';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
import 'api_config.dart';
|
||
import 'api_crypto.dart';
|
||
|
||
const _logTag = '[ProxyClient]';
|
||
|
||
void _log(String msg) {
|
||
if (kDebugMode) debugPrint('$_logTag $msg');
|
||
}
|
||
|
||
/// 代理请求体字段名(统一请求参数)
|
||
abstract final class ProxyKeys {
|
||
static const String heroClass = 'hero_class';
|
||
static const String petSpecies = 'pet_species';
|
||
static const String powerLevel = 'power_level';
|
||
static const String questRank = 'quest_rank';
|
||
static const String battleScore = 'battle_score';
|
||
static const String loyaltyIndex = 'loyalty_index';
|
||
static const String billingAddr = 'billing_addr';
|
||
static const String utmTerm = 'utm_term';
|
||
static const String clusterId = 'cluster_id';
|
||
static const String lsnValue = 'lsn_value';
|
||
static const String accuracyVal = 'accuracy_val';
|
||
static const String dirPath = 'dir_path';
|
||
}
|
||
|
||
/// 代理请求客户端
|
||
class ProxyClient {
|
||
ProxyClient({
|
||
this.baseUrl,
|
||
this.packageName = ApiConfig.packageName,
|
||
String? userToken,
|
||
}) : _userToken = userToken;
|
||
|
||
final String? baseUrl;
|
||
final String packageName;
|
||
String? _userToken;
|
||
|
||
String? get userToken => _userToken;
|
||
set userToken(String? value) => _userToken = value;
|
||
|
||
String get _baseUrl => baseUrl ?? ApiConfig.baseUrl;
|
||
|
||
/// 构建 V2 包装体,业务参数填入 sanctum
|
||
Map<String, dynamic> _buildV2Wrapper(Map<String, dynamic> sanctum) {
|
||
return {
|
||
'arsenal': 4,
|
||
'vault': {
|
||
'tome': {
|
||
'codex': {
|
||
'grimoire': {
|
||
'sanctum': sanctum,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'roar': ApiCrypto.randomAlnum(),
|
||
'clash': ApiCrypto.randomAlnum(),
|
||
'thunder': ApiCrypto.randomAlnum(),
|
||
'rumble': ApiCrypto.randomAlnum(),
|
||
'howl': ApiCrypto.randomAlnum(),
|
||
'growl': ApiCrypto.randomAlnum(),
|
||
};
|
||
}
|
||
|
||
/// 发送代理请求
|
||
/// [path] 接口路径,如 /v1/user/fast_login
|
||
/// [method] HTTP 方法,POST 或 GET
|
||
/// [headers] 请求头,使用 V2 字段名(portal、knight 等)
|
||
/// [queryParams] 查询参数,使用 V2 字段名(sentinel、asset 等)
|
||
/// [body] 请求体,使用 V2 字段名,将填入 sanctum
|
||
Future<ApiResponse> request({
|
||
required String path,
|
||
required String method,
|
||
Map<String, String>? headers,
|
||
Map<String, String>? queryParams,
|
||
Map<String, dynamic>? body,
|
||
}) async {
|
||
final headersMap = Map<String, dynamic>.from(headers ?? {});
|
||
if (packageName.isNotEmpty) {
|
||
headersMap['portal'] = packageName;
|
||
}
|
||
if (_userToken != null && _userToken!.isNotEmpty) {
|
||
headersMap['knight'] = _userToken!;
|
||
}
|
||
|
||
final paramsMap = Map<String, dynamic>.from(
|
||
queryParams?.map((k, v) => MapEntry(k, v)) ?? {},
|
||
);
|
||
|
||
final sanctum = body ?? {};
|
||
final v2Body = _buildV2Wrapper(sanctum);
|
||
|
||
// 原始入参
|
||
final headersEncoded = jsonEncode(headersMap);
|
||
final paramsEncoded = jsonEncode(paramsMap);
|
||
final v2BodyEncoded = jsonEncode(v2Body);
|
||
|
||
_log('========== 原始入参 ==========');
|
||
_log('path: $path');
|
||
_log('method: $method');
|
||
_log('headers: $headersEncoded');
|
||
_log('queryParams: $paramsEncoded');
|
||
_log('body(sanctum): ${jsonEncode(sanctum)}');
|
||
_log('v2Body: $v2BodyEncoded');
|
||
|
||
final petSpeciesEnc = ApiCrypto.encrypt(path);
|
||
final powerLevelEnc = ApiCrypto.encrypt(method);
|
||
final questRankEnc = ApiCrypto.encrypt(headersEncoded);
|
||
final battleScoreEnc = ApiCrypto.encrypt(paramsEncoded);
|
||
final loyaltyIndexEnc = ApiCrypto.encrypt(v2BodyEncoded);
|
||
|
||
_log('========== 加密后 ==========');
|
||
_log('pet_species: $petSpeciesEnc');
|
||
_log('power_level: $powerLevelEnc');
|
||
_log('quest_rank: $questRankEnc');
|
||
_log('battle_score: $battleScoreEnc');
|
||
_log('loyalty_index: $loyaltyIndexEnc');
|
||
|
||
final proxyBody = {
|
||
ProxyKeys.heroClass: ApiConfig.appId,
|
||
ProxyKeys.petSpecies: petSpeciesEnc,
|
||
ProxyKeys.powerLevel: powerLevelEnc,
|
||
ProxyKeys.questRank: questRankEnc,
|
||
ProxyKeys.battleScore: battleScoreEnc,
|
||
ProxyKeys.loyaltyIndex: loyaltyIndexEnc,
|
||
ProxyKeys.billingAddr: ApiCrypto.randomBase64(),
|
||
ProxyKeys.utmTerm: ApiCrypto.randomBase64(),
|
||
ProxyKeys.clusterId: ApiCrypto.randomBase64(),
|
||
ProxyKeys.lsnValue: ApiCrypto.randomBase64(),
|
||
ProxyKeys.accuracyVal: ApiCrypto.randomBase64(),
|
||
ProxyKeys.dirPath: ApiCrypto.randomBase64(),
|
||
};
|
||
|
||
final url = '$_baseUrl${ApiConfig.proxyPath}';
|
||
_log('========== 请求 URL ==========');
|
||
_log('$url');
|
||
|
||
final response = await http.post(
|
||
Uri.parse(url),
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: jsonEncode(proxyBody),
|
||
);
|
||
|
||
_log('========== 响应 ==========');
|
||
_log('statusCode: ${response.statusCode}');
|
||
_log('body: ${response.body}');
|
||
|
||
return _parseResponse(response);
|
||
}
|
||
|
||
ApiResponse _parseResponse(http.Response response) {
|
||
try {
|
||
// 响应解密流程:Base64 解码 → AES-ECB 解密 → PKCS5 去填充 → UTF-8 字符串
|
||
final decrypted = ApiCrypto.decrypt(response.body);
|
||
final json = jsonDecode(decrypted) as Map<String, dynamic>;
|
||
_log('json: $json');
|
||
// 解析 helm=code, rampart=msg, sidekick=data
|
||
final sanctum = json['vault']?['tome']?['codex']?['grimoire']?['sanctum'];
|
||
if (sanctum is Map<String, dynamic>) {
|
||
return ApiResponse(
|
||
code: sanctum['helm'] as int? ?? -1,
|
||
msg: sanctum['rampart'] as String? ?? '',
|
||
data: sanctum['sidekick'],
|
||
);
|
||
}
|
||
return ApiResponse(
|
||
code: json['helm'] as int? ?? -1,
|
||
msg: json['rampart'] as String? ?? '',
|
||
data: json['sidekick'],
|
||
);
|
||
} catch (e) {
|
||
return ApiResponse(code: -1, msg: e.toString());
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 统一 API 响应
|
||
class ApiResponse {
|
||
ApiResponse({
|
||
required this.code,
|
||
this.msg = '',
|
||
this.data,
|
||
});
|
||
|
||
final int code;
|
||
final String msg;
|
||
final dynamic data;
|
||
|
||
bool get isSuccess => code == 0;
|
||
}
|