From ecf4326b47c6e2bed61bdf61095d380f57c45d4b Mon Sep 17 00:00:00 2001 From: Imran Remtulla Date: Sat, 26 Apr 2025 23:48:15 -0400 Subject: [PATCH] =?UTF-8?q?Always=20follow=20redirects=20and=20store=20coo?= =?UTF-8?q?kies=20between=20redirects,=20including=20for=20downloads=20?= =?UTF-8?q?=E2=80=94useful=20for=20https://xeiaso.net/blog/2025/anubis=20(?= =?UTF-8?q?#2264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/providers/apps_provider.dart | 49 ++++------- lib/providers/source_provider.dart | 134 +++++++++++++++++------------ 2 files changed, 93 insertions(+), 90 deletions(-) diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index 833f1c7..3fe46be 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'dart:math'; import 'package:battery_plus/battery_plus.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; import 'dart:typed_data'; @@ -246,9 +245,9 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, 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; + var headersClient = IOClient(createHttpClient(allowInsecure)); + StreamedResponse headersResponse = await headersClient.send(req); + var resHeaders = headersResponse.headers; // Use the headers to decide what the file extension is, and // whether it supports partial downloads (range request), and @@ -276,21 +275,20 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, rangeFeatureEnabled = resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes'; } + headersClient.close(); // If you have an existing file that is usable, // decide whether you can use it (either return full or resume partial) - var fullContentLength = response.contentLength; + var fullContentLength = headersResponse.contentLength; if (useExisting && downloadedFile.existsSync()) { var length = downloadedFile.lengthSync(); if (fullContentLength == null || !rangeFeatureEnabled) { // If there is no content length reported, assume it the existing file is fully downloaded // Also if the range feature is not supported, don't trust the content length if any (#1542) - client.close(); return downloadedFile; } else { // Check if resume needed/possible if (length == fullContentLength) { - client.close(); return downloadedFile; } if (length > fullContentLength) { @@ -330,7 +328,6 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, if (shouldReturn) { logs?.add( 'Existing partial download completed - not repeating: ${tempDownloadedFile.uri.pathSegments.last}'); - client.close(); return downloadedFile; } else { logs?.add( @@ -346,17 +343,18 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, : null; int rangeStart = targetFileLength ?? 0; IOSink? sink; + req = Request('GET', Uri.parse(url)); + req.headers.addAll(reqHeaders); if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) { - client.close(); - client = IOClient(createHttpClient(allowInsecure)); - req = Request('GET', Uri.parse(url)); - req.headers.addAll(reqHeaders); - req.headers.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'}); - response = await client.send(req); + reqHeaders.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'}); sink = tempDownloadedFile.openWrite(mode: FileMode.writeOnlyAppend); } else if (tempDownloadedFile.existsSync()) { tempDownloadedFile.deleteSync(recursive: true); } + var responseWithClient = + await sourceRequestStreamResponse('GET', url, reqHeaders, {}); + HttpClient responseClient = responseWithClient.key; + HttpClientResponse response = responseWithClient.value; sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly); // Perform the download @@ -369,7 +367,8 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, const downloadUIUpdateInterval = Duration(milliseconds: 500); const downloadBufferSize = 32 * 1024; // 32KB final downloadBuffer = BytesBuilder(); - await response.stream + await response + .asBroadcastStream() .map((chunk) { received += chunk.length; final now = DateTime.now(); @@ -407,31 +406,15 @@ Future downloadFile(String url, String fileName, bool fileNameHasExt, } if (response.statusCode < 200 || response.statusCode > 299) { tempDownloadedFile.deleteSync(recursive: true); - throw response.reasonPhrase ?? tr('unexpectedError'); + throw response.reasonPhrase; } if (tempDownloadedFile.existsSync()) { tempDownloadedFile.renameSync(downloadedFile.path); } - client.close(); + responseClient.close(); return downloadedFile; } -Future> getHeaders(String url, - {Map? headers, bool allowInsecure = false}) async { - var req = http.Request('GET', Uri.parse(url)); - if (headers != null) { - req.headers.addAll(headers); - } - var client = IOClient(createHttpClient(allowInsecure)); - var response = await client.send(req); - if (response.statusCode < 200 || response.statusCode > 299) { - throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError')); - } - var returnHeaders = response.headers; - client.close(); - return returnHeaders; -} - Future> getAllInstalledInfo() async { return await pm.getInstalledPackages() ?? []; } diff --git a/lib/providers/source_provider.dart b/lib/providers/source_provider.dart index 423b1d3..a70d0d3 100644 --- a/lib/providers/source_provider.dart +++ b/lib/providers/source_provider.dart @@ -510,6 +510,75 @@ HttpClient createHttpClient(bool insecure) { return client; } +Future> sourceRequestStreamResponse( + String method, + String url, + Map? requestHeaders, + Map additionalSettings, + {bool followRedirects = true, + Object? postBody}) async { + var currentUrl = Uri.parse(url); + var redirectCount = 0; + const maxRedirects = 10; + List cookies = []; + while (redirectCount < maxRedirects) { + var httpClient = + createHttpClient(additionalSettings['allowInsecure'] == true); + var request = await httpClient.openUrl(method, currentUrl); + if (requestHeaders != null) { + requestHeaders.forEach((key, value) { + request.headers.set(key, value); + }); + } + request.cookies.addAll(cookies); + request.followRedirects = false; + if (postBody != null) { + request.headers.contentType = ContentType.json; + request.write(jsonEncode(postBody)); + } + final response = await request.close(); + + if (followRedirects && + (response.statusCode >= 300 && response.statusCode <= 399)) { + final location = response.headers.value(HttpHeaders.locationHeader); + if (location != null) { + currentUrl = Uri.parse(ensureAbsoluteUrl(location, currentUrl)); + redirectCount++; + cookies = response.cookies; + httpClient.close(); + continue; + } + } + + return MapEntry(httpClient, response); + } + throw ObtainiumError('Too many redirects ($maxRedirects)'); + } + + Future httpClientResponseStreamToFinalResponse( + HttpClient httpClient, + String method, + String url, + HttpClientResponse response) async { + final bytes = + (await response.fold(BytesBuilder(), (b, d) => b..add(d))) + .toBytes(); + + final headers = {}; + 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)), + ); + } + abstract class AppSource { List hosts = []; bool hostChanged = false; @@ -567,64 +636,15 @@ abstract class AppSource { Future sourceRequest( String url, Map additionalSettings, {bool followRedirects = true, Object? postBody}) async { + var method = postBody == null ? 'GET' : 'POST'; var requestHeaders = await getRequestHeaders(additionalSettings); - - if (requestHeaders != null || followRedirects == false) { - var method = postBody == null ? 'GET' : 'POST'; - var currentUrl = url; - var redirectCount = 0; - 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(), (b, d) => b..add(d))) - .toBytes(); - - final headers = {}; - 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)), - ); - } - throw ObtainiumError('Too many redirects ($maxRedirects)'); - } else { - return postBody == null - ? http.get(Uri.parse(url)) - : http.post(Uri.parse(url), body: jsonEncode(postBody)); - } + var streamedResponseAndClient = await sourceRequestStreamResponse( + method, url, requestHeaders, additionalSettings); + return await httpClientResponseStreamToFinalResponse( + streamedResponseAndClient.key, + method, + url, + streamedResponseAndClient.value); } void runOnAddAppInputChange(String inputUrl) {