mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-12 21:06:43 +02:00
194 lines
6.1 KiB
Dart
194 lines
6.1 KiB
Dart
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<String?> tryInferringAppId(
|
|
String standardUrl, {
|
|
Map<String, dynamic> additionalSettings = const {},
|
|
}) async {
|
|
String appId = Uri.parse(standardUrl).pathSegments.last;
|
|
return appId;
|
|
}
|
|
|
|
@override
|
|
Future<APKDetails> getLatestAPKDetails(
|
|
String standardUrl,
|
|
Map<String, dynamic> 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<String> _getLatestApkUrl(
|
|
String apiUrl,
|
|
String appId,
|
|
String aid,
|
|
String version,
|
|
Map<String, String>? 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<Map<String, String>?> getRequestHeaders(
|
|
Map<String, dynamic> 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<String, String> _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};
|
|
}
|
|
}
|