diff --git a/README.md b/README.md index b0985e6..6518041 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Currently supported App sources: - [Uptodown](https://uptodown.com/) - [Huawei AppGallery](https://appgallery.huawei.com/) - [Tencent App Store](https://sj.qq.com/) + - [CoolApk](https://coolapk.com/) - [RuStore](https://rustore.ru/) - Jenkins Jobs - [APKMirror](https://apkmirror.com/) (Track-Only) diff --git a/assets/translations/en.json b/assets/translations/en.json index 260daf4..30ff154 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -320,6 +320,7 @@ "stayOneVersionBehind": "Stay one version behind latest", "refreshBeforeDownload": "Refresh app details before download", "tencentAppStore": "Tencent App Store", + "coolApk": "CoolApk", "name": "Name", "smartname": "Name (smart)", "sortMethod": "Sort method", diff --git a/assets/translations/zh.json b/assets/translations/zh.json index b5e37d4..4e617a0 100644 --- a/assets/translations/zh.json +++ b/assets/translations/zh.json @@ -320,6 +320,7 @@ "stayOneVersionBehind": "比最新版本晚一个版本", "refreshBeforeDownload": "下载前刷新应用程序详细信息", "tencentAppStore": "腾讯应用宝", + "coolApk": "酷安", "name": "名称", "smartname": "姓名(智能)", "sortMethod": "排序方法", diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 90025e2..d620444 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -25,6 +25,7 @@
  • APKMirror (Track-Only)
  • Huawei AppGallery
  • Tencent App Store
  • +
  • CoolApk
  • Jenkins Jobs
  • RuStore
  • diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt index e762156..7249abd 100644 --- a/fastlane/metadata/android/ru/full_description.txt +++ b/fastlane/metadata/android/ru/full_description.txt @@ -25,6 +25,7 @@
  • APKMirror (Track-Only)
  • Huawei AppGallery
  • Tencent App Store
  • +
  • CoolApk
  • Jenkins Jobs
  • RuStore
  • diff --git a/lib/app_sources/coolapk.dart b/lib/app_sources/coolapk.dart new file mode 100644 index 0000000..01027da --- /dev/null +++ b/lib/app_sources/coolapk.dart @@ -0,0 +1,173 @@ +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; + } + + @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}; + } +} \ No newline at end of file diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index f9cec27..423b1d3 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -14,6 +14,7 @@ import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkpure.dart'; import 'package:obtainium/app_sources/aptoide.dart'; import 'package:obtainium/app_sources/codeberg.dart'; +import 'package:obtainium/app_sources/coolapk.dart'; import 'package:obtainium/app_sources/directAPKLink.dart'; import 'package:obtainium/app_sources/fdroid.dart'; import 'package:obtainium/app_sources/fdroidrepo.dart'; @@ -934,6 +935,7 @@ class SourceProvider { Uptodown(), HuaweiAppGallery(), Tencent(), + CoolApk(), Jenkins(), APKMirror(), RuStore(), diff --git a/pubspec.lock b/pubspec.lock index fdbe19d..8b3ca50 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -124,6 +124,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + bcrypt: + dependency: "direct main" + description: + name: bcrypt + sha256: "9dc3f234d5935a76917a6056613e1a6d9b53f7fa56f98e24cd49b8969307764b" + url: "https://pub.dev" + source: hosted + version: "1.1.3" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a2934e4..1cc6429 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: url: https://github.com/AlexBacich/shared-storage ref: master crypto: ^3.0.3 + bcrypt: ^1.1.3 app_links: ^6.0.1 background_fetch: ^1.2.1 equations: ^5.0.2