mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-16 14:46:44 +02:00
Merge pull request #2225 from ImranR98/dev
- Ensure headers are still sent when URL request is redirected (#1973) - Add 'ETag header' option for HTML and direct APK links (#2221) - Ensure links on add app page do not overlap/merge (#2216)
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:obtainium/app_sources/html.dart';
|
import 'package:obtainium/app_sources/html.dart';
|
||||||
|
import 'package:obtainium/components/generated_form.dart';
|
||||||
import 'package:obtainium/custom_errors.dart';
|
import 'package:obtainium/custom_errors.dart';
|
||||||
import 'package:obtainium/providers/source_provider.dart';
|
import 'package:obtainium/providers/source_provider.dart';
|
||||||
|
|
||||||
@ -8,12 +9,23 @@ class DirectAPKLink extends AppSource {
|
|||||||
|
|
||||||
DirectAPKLink() {
|
DirectAPKLink() {
|
||||||
name = tr('directAPKLink');
|
name = tr('directAPKLink');
|
||||||
additionalSourceAppSpecificSettingFormItems = html
|
additionalSourceAppSpecificSettingFormItems = [
|
||||||
.additionalSourceAppSpecificSettingFormItems
|
...html.additionalSourceAppSpecificSettingFormItems
|
||||||
.where((element) => element
|
.where((element) => element
|
||||||
.where((element) => element.key == 'requestHeader')
|
.where((element) => element.key == 'requestHeader')
|
||||||
.isNotEmpty)
|
.isNotEmpty)
|
||||||
.toList();
|
.toList(),
|
||||||
|
[
|
||||||
|
GeneratedFormDropdown(
|
||||||
|
'defaultPseudoVersioningMethod',
|
||||||
|
[
|
||||||
|
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||||
|
MapEntry('ETag', 'ETag')
|
||||||
|
],
|
||||||
|
label: tr('defaultPseudoVersioningMethod'),
|
||||||
|
defaultValue: 'partialAPKHash')
|
||||||
|
]
|
||||||
|
];
|
||||||
excludeCommonSettingKeys = [
|
excludeCommonSettingKeys = [
|
||||||
'versionExtractionRegEx',
|
'versionExtractionRegEx',
|
||||||
'matchGroupToUse',
|
'matchGroupToUse',
|
||||||
@ -57,9 +69,8 @@ class DirectAPKLink extends AppSource {
|
|||||||
additionalSettingsNew[s] = additionalSettings[s];
|
additionalSettingsNew[s] = additionalSettings[s];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
additionalSettingsNew['defaultPseudoVersioningMethod'] = 'partialAPKHash';
|
|
||||||
additionalSettingsNew['directAPKLink'] = true;
|
additionalSettingsNew['directAPKLink'] = true;
|
||||||
additionalSettings['versionDetection'] = false;
|
additionalSettingsNew['versionDetection'] = false;
|
||||||
return html.getLatestAPKDetails(standardUrl, additionalSettingsNew);
|
return html.getLatestAPKDetails(standardUrl, additionalSettingsNew);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,8 @@ class HTML extends AppSource {
|
|||||||
'defaultPseudoVersioningMethod',
|
'defaultPseudoVersioningMethod',
|
||||||
[
|
[
|
||||||
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
MapEntry('partialAPKHash', tr('partialAPKHash')),
|
||||||
MapEntry('APKLinkHash', tr('APKLinkHash'))
|
MapEntry('APKLinkHash', tr('APKLinkHash')),
|
||||||
|
MapEntry('ETag', 'ETag')
|
||||||
],
|
],
|
||||||
label: tr('defaultPseudoVersioningMethod'),
|
label: tr('defaultPseudoVersioningMethod'),
|
||||||
defaultValue: 'partialAPKHash')
|
defaultValue: 'partialAPKHash')
|
||||||
@ -356,14 +357,24 @@ class HTML extends AppSource {
|
|||||||
additionalSettings['versionExtractWholePage'] == true
|
additionalSettings['versionExtractWholePage'] == true
|
||||||
? versionExtractionWholePageString
|
? versionExtractionWholePageString
|
||||||
: relDecoded);
|
: relDecoded);
|
||||||
version ??= additionalSettings['defaultPseudoVersioningMethod'] ==
|
var apkReqHeaders =
|
||||||
'APKLinkHash'
|
await getRequestHeaders(additionalSettings, forAPKDownload: true);
|
||||||
? rel.hashCode.toString()
|
if (version == null &&
|
||||||
: (await checkPartialDownloadHashDynamic(rel,
|
additionalSettings['defaultPseudoVersioningMethod'] == 'ETag') {
|
||||||
headers: await getRequestHeaders(additionalSettings,
|
version = await checkETagHeader(rel,
|
||||||
forAPKDownload: true),
|
headers: apkReqHeaders,
|
||||||
allowInsecure: additionalSettings['allowInsecure'] == true))
|
allowInsecure: additionalSettings['allowInsecure'] == true);
|
||||||
.toString();
|
if (version == null) {
|
||||||
|
throw NoVersionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
version ??=
|
||||||
|
additionalSettings['defaultPseudoVersioningMethod'] == 'APKLinkHash'
|
||||||
|
? rel.hashCode.toString()
|
||||||
|
: (await checkPartialDownloadHashDynamic(rel,
|
||||||
|
headers: apkReqHeaders,
|
||||||
|
allowInsecure: additionalSettings['allowInsecure'] == true))
|
||||||
|
.toString();
|
||||||
return APKDetails(
|
return APKDetails(
|
||||||
version,
|
version,
|
||||||
[rel].map((e) {
|
[rel].map((e) {
|
||||||
|
@ -46,6 +46,7 @@ List<MapEntry<Locale, String>> supportedLocales = const [
|
|||||||
'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
|
'Esperanto'), // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
|
||||||
MapEntry(Locale('in'), 'Bahasa Indonesia'),
|
MapEntry(Locale('in'), 'Bahasa Indonesia'),
|
||||||
MapEntry(Locale('ko'), '한국어'),
|
MapEntry(Locale('ko'), '한국어'),
|
||||||
|
MapEntry(Locale('ca'), 'Català'),
|
||||||
];
|
];
|
||||||
const fallbackLocale = Locale('en');
|
const fallbackLocale = Locale('en');
|
||||||
const localeDir = 'assets/translations';
|
const localeDir = 'assets/translations';
|
||||||
|
@ -575,8 +575,10 @@ class AddAppPageState extends State<AddAppPage> {
|
|||||||
|
|
||||||
Widget getSourcesListWidget() => Padding(
|
Widget getSourcesListWidget() => Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
direction: Axis.horizontal,
|
||||||
|
alignment: WrapAlignment.spaceBetween,
|
||||||
|
spacing: 12,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -220,6 +220,22 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
|
|||||||
return hashListOfLists(bytes);
|
return hashListOfLists(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> checkETagHeader(String url,
|
||||||
|
{Map<String, String>? headers, bool allowInsecure = false}) async {
|
||||||
|
// Send the initial request but cancel it as soon as you have the headers
|
||||||
|
var reqHeaders = headers ?? {};
|
||||||
|
var req = Request('GET', Uri.parse(url));
|
||||||
|
req.headers.addAll(reqHeaders);
|
||||||
|
var client = IOClient(createHttpClient(allowInsecure));
|
||||||
|
StreamedResponse response = await client.send(req);
|
||||||
|
var resHeaders = response.headers;
|
||||||
|
client.close();
|
||||||
|
return resHeaders[HttpHeaders.etagHeader]
|
||||||
|
?.replaceAll('"', '')
|
||||||
|
.hashCode
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
Future<File> downloadFile(String url, String fileName, bool fileNameHasExt,
|
Future<File> downloadFile(String url, String fileName, bool fileNameHasExt,
|
||||||
Function? onProgress, String destDir,
|
Function? onProgress, String destDir,
|
||||||
{bool useExisting = true,
|
{bool useExisting = true,
|
||||||
|
@ -3,12 +3,13 @@
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:http/io_client.dart';
|
|
||||||
import 'package:obtainium/app_sources/apkmirror.dart';
|
import 'package:obtainium/app_sources/apkmirror.dart';
|
||||||
import 'package:obtainium/app_sources/apkpure.dart';
|
import 'package:obtainium/app_sources/apkpure.dart';
|
||||||
import 'package:obtainium/app_sources/aptoide.dart';
|
import 'package:obtainium/app_sources/aptoide.dart';
|
||||||
@ -566,23 +567,62 @@ abstract class AppSource {
|
|||||||
String url, Map<String, dynamic> additionalSettings,
|
String url, Map<String, dynamic> additionalSettings,
|
||||||
{bool followRedirects = true, Object? postBody}) async {
|
{bool followRedirects = true, Object? postBody}) async {
|
||||||
var requestHeaders = await getRequestHeaders(additionalSettings);
|
var requestHeaders = await getRequestHeaders(additionalSettings);
|
||||||
|
|
||||||
if (requestHeaders != null || followRedirects == false) {
|
if (requestHeaders != null || followRedirects == false) {
|
||||||
var req = Request(postBody == null ? 'GET' : 'POST', Uri.parse(url));
|
var method = postBody == null ? 'GET' : 'POST';
|
||||||
req.followRedirects = followRedirects;
|
var currentUrl = url;
|
||||||
if (requestHeaders != null) {
|
var redirectCount = 0;
|
||||||
req.headers.addAll(requestHeaders);
|
const maxRedirects = 10;
|
||||||
|
while (redirectCount < maxRedirects) {
|
||||||
|
var httpClient =
|
||||||
|
createHttpClient(additionalSettings['allowInsecure'] == true);
|
||||||
|
var request = await httpClient.openUrl(method, Uri.parse(currentUrl));
|
||||||
|
if (requestHeaders != null) {
|
||||||
|
requestHeaders.forEach((key, value) {
|
||||||
|
request.headers.set(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
request.followRedirects = false;
|
||||||
|
if (postBody != null) {
|
||||||
|
request.headers.contentType = ContentType.json;
|
||||||
|
request.write(jsonEncode(postBody));
|
||||||
|
}
|
||||||
|
final response = await request.close();
|
||||||
|
|
||||||
|
if (followRedirects &&
|
||||||
|
(response.statusCode == 301 || response.statusCode == 302)) {
|
||||||
|
final location = response.headers.value(HttpHeaders.locationHeader);
|
||||||
|
if (location != null) {
|
||||||
|
currentUrl = location;
|
||||||
|
redirectCount++;
|
||||||
|
httpClient.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = (await response.fold<BytesBuilder>(
|
||||||
|
BytesBuilder(), (b, d) => b..add(d)))
|
||||||
|
.toBytes();
|
||||||
|
|
||||||
|
final headers = <String, String>{};
|
||||||
|
response.headers.forEach((name, values) {
|
||||||
|
headers[name] = values.join(', ');
|
||||||
|
});
|
||||||
|
|
||||||
|
httpClient.close();
|
||||||
|
|
||||||
|
return http.Response.bytes(
|
||||||
|
bytes,
|
||||||
|
response.statusCode,
|
||||||
|
headers: headers,
|
||||||
|
request: http.Request(method, Uri.parse(url)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (postBody != null) {
|
throw ObtainiumError('Too many redirects ($maxRedirects)');
|
||||||
req.headers[HttpHeaders.contentTypeHeader] = 'application/json';
|
|
||||||
req.body = jsonEncode(postBody);
|
|
||||||
}
|
|
||||||
return Response.fromStream(await IOClient(
|
|
||||||
createHttpClient(additionalSettings['allowInsecure'] == true))
|
|
||||||
.send(req));
|
|
||||||
} else {
|
} else {
|
||||||
return postBody == null
|
return postBody == null
|
||||||
? get(Uri.parse(url))
|
? http.get(Uri.parse(url))
|
||||||
: post(Uri.parse(url), body: jsonEncode(postBody));
|
: http.post(Uri.parse(url), body: jsonEncode(postBody));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
16
pubspec.lock
16
pubspec.lock
@ -304,10 +304,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "09b474c0c8117484b80cbebc043801ff91e05cfbd2874d512825c899e1754694"
|
sha256: "36a1652d99cb6bf8ccc8b9f43aded1fd60b234d23ce78af422c07f950a436ef7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.2.3"
|
version: "10.0.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -490,10 +490,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
sha256: e7bbc718adc9476aa14cfddc1ef048d2e21e4e8f18311aaac723266db9f9e7b5
|
sha256: "634622a3a826d67cb05c0e3e576d1812c430fa98404e95b60b131775c73d76ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6+2"
|
version: "0.7.7"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1107,10 +1107,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.2"
|
version: "6.3.3"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1211,10 +1211,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: c49a98510080378b1525132f407a92c3dcd3b7145bef04fb8137724aadcf1cf0
|
sha256: c14455137ce60a68e1ccaf4e8f2dae8cebcb3465ddaa2fcfb57584fb7c5afe4d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.18.4"
|
version: "3.18.5"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.48+2305
|
version: 1.1.49+2306
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.6.0
|
sdk: ^3.6.0
|
||||||
@ -47,7 +47,7 @@ dependencies:
|
|||||||
permission_handler: ^11.0.0
|
permission_handler: ^11.0.0
|
||||||
fluttertoast: ^8.0.9
|
fluttertoast: ^8.0.9
|
||||||
device_info_plus: ^11.0.0
|
device_info_plus: ^11.0.0
|
||||||
file_picker: ^9.0.0
|
file_picker: ^10.0.0
|
||||||
animations: ^2.0.4
|
animations: ^2.0.4
|
||||||
android_package_installer: # TODO: See if PR will be accepted (dev may not be active), else remove this comment
|
android_package_installer: # TODO: See if PR will be accepted (dev may not be active), else remove this comment
|
||||||
git:
|
git:
|
||||||
|
Reference in New Issue
Block a user