import 'dart:convert'; import 'package:bcrypt/bcrypt.dart'; import 'package:crypto/crypto.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'dart:math'; // kanged from https://github.com/DUpdateSystem/UpgradeAll/blob/b2f92c9/core-websdk/src/main/java/net/xzos/upgradeall/core/websdk/api/client_proxy/hubs/CoolApk.kt class CoolApk extends AppSource { CoolApk() { name = tr('coolApk'); hosts = ['www.coolapk.com', 'api2.coolapk.com']; allowSubDomains = true; naiveStandardVersionDetection = true; allowOverride = false; } @override String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) { RegExp standardUrlRegEx = RegExp( r'^https?://(www\.)?coolapk\.com/apk/[^/]+', caseSensitive: false, ); var match = standardUrlRegEx.firstMatch(url); if (match == null) { throw InvalidURLError(name); } String standardizedUrl = match.group(0)!; return standardizedUrl; } @override Future tryInferringAppId( String standardUrl, { Map additionalSettings = const {}, }) async { String appId = Uri.parse(standardUrl).pathSegments.last; return appId; } @override Future getLatestAPKDetails( String standardUrl, Map additionalSettings, ) async { String appId = (await tryInferringAppId(standardUrl))!; String apiUrl = 'https://api2.coolapk.com'; // get latest var detailUrl = '$apiUrl/v6/apk/detail?id=$appId'; var headers = await getRequestHeaders(additionalSettings); var res = await sourceRequest(detailUrl, additionalSettings); if (res.statusCode != 200) { throw getObtainiumHttpError(res); } var json = jsonDecode(res.body); if (json['status'] == -2 || json['data'] == null) { throw NoReleasesError(); } var detail = json['data']; String version = detail['apkversionname'].toString(); String appName = detail['title'].toString(); String author = detail['developername']?.toString() ?? 'CoolApk'; String changelog = detail['changelog']?.toString() ?? ''; int? releaseDate = detail['lastupdate'] != null ? (detail['lastupdate'] is int ? detail['lastupdate'] * 1000 : int.parse(detail['lastupdate'].toString()) * 1000) : null; String aid = detail['id'].toString(); // get apk url String apkUrl = await _getLatestApkUrl( apiUrl, appId, aid, version, headers, ); if (apkUrl.isEmpty) { throw NoAPKError(); } String apkName = '${appId}_$version.apk'; return APKDetails( version, [MapEntry(apkName, apkUrl)], AppNames(author, appName), releaseDate: releaseDate != null ? DateTime.fromMillisecondsSinceEpoch(releaseDate) : null, changeLog: changelog, ); } Future _getLatestApkUrl( String apiUrl, String appId, String aid, String version, Map? headers, ) async { String url = '$apiUrl/v6/apk/download?pn=$appId&aid=$aid'; var res = await sourceRequest(url, {}, followRedirects: false); if (res.statusCode >= 300 && res.statusCode < 400) { String location = res.headers['location'] ?? ''; return location; } return ''; } @override Future?> getRequestHeaders( Map additionalSettings, { bool forAPKDownload = false, }) async { var tokenPair = _getToken(); // CoolAPK header return { 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/12.4.2-2208241-universal', 'X-App-Id': 'com.coolapk.market', 'X-Requested-With': 'XMLHttpRequest', 'X-Sdk-Int': '30', 'X-App-Mode': 'universal', 'X-App-Channel': 'coolapk', 'X-Sdk-Locale': 'zh-CN', 'X-App-Version': '12.4.2', 'X-Api-Supported': '2208241', 'X-App-Code': '2208241', 'X-Api-Version': '12', 'X-App-Device': tokenPair['deviceCode']!, 'X-Dark-Mode': '0', 'X-App-Token': tokenPair['token']!, }; } Map _getToken() { final rand = Random(); String randHexString(int n) => List.generate( n, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'), ).join().toUpperCase(); String randMacAddress() => List.generate( 6, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0'), ).join(':'); // 加密算法来自 https://github.com/XiaoMengXinX/FuckCoolapkTokenV2、https://github.com/Coolapk-UWP/Coolapk-UWP // device String aid = randHexString(16); String mac = randMacAddress(); const manufactor = 'Google'; const brand = 'Google'; const model = 'Pixel 5a'; const buildNumber = 'SQ1D.220105.007'; // generate deviceCode String deviceCode = base64.encode( '$aid; ; ; $mac; $manufactor; $brand; $model; $buildNumber'.codeUnits, ); // generate timestamp String timeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000) .toString(); String base64TimeStamp = base64.encode(timeStamp.codeUnits); String md5TimeStamp = md5.convert(timeStamp.codeUnits).toString(); String md5DeviceCode = md5.convert(deviceCode.codeUnits).toString(); // generate token String token = 'token://com.coolapk.market/dcf01e569c1e3db93a3d0fcf191a622c?$md5TimeStamp\$$md5DeviceCode&com.coolapk.market'; String base64Token = base64.encode(token.codeUnits); String md5Base64Token = md5.convert(base64Token.codeUnits).toString(); String md5Token = md5.convert(token.codeUnits).toString(); // generate salt and hash String bcryptSalt = '\$2a\$10\$${base64TimeStamp.substring(0, 14)}/${md5Token.substring(0, 6)}u'; String bcryptResult = BCrypt.hashpw(md5Base64Token, bcryptSalt); String reBcryptResult = bcryptResult.replaceRange(0, 3, '\$2y'); String finalToken = 'v2${base64.encode(reBcryptResult.codeUnits)}'; return {'deviceCode': deviceCode, 'token': finalToken}; } }