diff --git a/assets/libcurl/android/arm64-v8a/libcurl.so b/assets/libcurl/android/arm64-v8a/libcurl.so new file mode 100755 index 000000000..a8b785452 Binary files /dev/null and b/assets/libcurl/android/arm64-v8a/libcurl.so differ diff --git a/assets/libcurl/android/arm64-v8a/libquiche.so b/assets/libcurl/android/arm64-v8a/libquiche.so new file mode 100755 index 000000000..9c9e5d8df Binary files /dev/null and b/assets/libcurl/android/arm64-v8a/libquiche.so differ diff --git a/assets/libcurl/android/armeabi-v7a/libcurl.so b/assets/libcurl/android/armeabi-v7a/libcurl.so new file mode 100755 index 000000000..4d1830d26 Binary files /dev/null and b/assets/libcurl/android/armeabi-v7a/libcurl.so differ diff --git a/assets/libcurl/android/armeabi-v7a/libquiche.so b/assets/libcurl/android/armeabi-v7a/libquiche.so new file mode 100755 index 000000000..f2ed1a62a Binary files /dev/null and b/assets/libcurl/android/armeabi-v7a/libquiche.so differ diff --git a/assets/libcurl/android/x86_64/libcurl.so b/assets/libcurl/android/x86_64/libcurl.so new file mode 100755 index 000000000..afaed085d Binary files /dev/null and b/assets/libcurl/android/x86_64/libcurl.so differ diff --git a/assets/libcurl/android/x86_64/libquiche.so b/assets/libcurl/android/x86_64/libquiche.so new file mode 100755 index 000000000..470e95d11 Binary files /dev/null and b/assets/libcurl/android/x86_64/libquiche.so differ diff --git a/assets/locale/en.json b/assets/locale/en.json index 155a2c326..2ced705bd 100644 --- a/assets/locale/en.json +++ b/assets/locale/en.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/eo.json b/assets/locale/eo.json index 56d11215c..6b1c1387a 100644 --- a/assets/locale/eo.json +++ b/assets/locale/eo.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/it.json b/assets/locale/it.json index 66ad02388..6e5a5577c 100644 --- a/assets/locale/it.json +++ b/assets/locale/it.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/ja.json b/assets/locale/ja.json index 6dae111ad..5cd86dfee 100644 --- a/assets/locale/ja.json +++ b/assets/locale/ja.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/ko.json b/assets/locale/ko.json index 152fd3976..05a2a5892 100644 --- a/assets/locale/ko.json +++ b/assets/locale/ko.json @@ -308,5 +308,6 @@ "cannotuseios": "iOS에선 사용할 수 없어요 :(", "exitTheApp": "앱 종료하기", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/pt.json b/assets/locale/pt.json index f582e6763..98cb63262 100644 --- a/assets/locale/pt.json +++ b/assets/locale/pt.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/zh.json b/assets/locale/zh.json index 8de7d1f38..a595b0404 100644 --- a/assets/locale/zh.json +++ b/assets/locale/zh.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/zh_Hans.json b/assets/locale/zh_Hans.json index 55c60e0ce..f2450bc35 100644 --- a/assets/locale/zh_Hans.json +++ b/assets/locale/zh_Hans.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/assets/locale/zh_Hant.json b/assets/locale/zh_Hant.json index 0afb327c8..7d45b84c1 100644 --- a/assets/locale/zh_Hant.json +++ b/assets/locale/zh_Hant.json @@ -308,5 +308,6 @@ "cannotuseios": "You cannot use update on iOS :(", "exitTheApp": "Exit the app", "deleteoldlogatstart": "Delete Old Log (At App Start)", - "ignorehttptimeout": "Ignore Http Timeout" + "ignorehttptimeout": "Ignore Http Timeout", + "usehttp3": "Use Http3 (Experimental)" } diff --git a/lib/network/libcurl.dart b/lib/network/libcurl.dart new file mode 100644 index 000000000..a9e9e8938 --- /dev/null +++ b/lib/network/libcurl.dart @@ -0,0 +1,177 @@ +// This source code is a part of Project Violet. +// Copyright (C) 2020-2023. violet-team. Licensed under the Apache-2.0 License. + +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter_curl/flutter_curl.dart' as flutter_curl; +import 'package:violet/log/log.dart'; +import 'package:violet/settings/settings.dart'; + +class Http3Request { + Future getClient() async { + late final soPath; + if(Platform.isAndroid) { + soPath = (await _checkSharedLibraryAndroid('libcurl'))!; + (await _checkSharedLibraryAndroid('libquiche'))!; + } else { + throw 'NOT_SUPPORTED'; + } + flutter_curl.Client client = flutter_curl.Client( + libPath: soPath, + httpVersions: [flutter_curl.HTTPVersion.http3] + ); + await client.init(); + return client; + } + + Future get( + String url,{ + Map? headers, + String? body, + Duration? timeout, + bool? followRedirects = true + } + ) async { + flutter_curl.Client client = await getClient(); + flutter_curl.Response? res; + do { + flutter_curl.Request req = flutter_curl.Request( + method: 'GET', + url: res?.headers['location'] ?? url, + headers: headers ?? {}, + body: (body?.isNotEmpty ?? false) ? flutter_curl.RequestBody.string(body ?? '') : null, + ); + if(res?.headers['location']?.isNotEmpty ?? false){ + req.headers.addAll({'Referer': req.url}); + } + Logger.info('[Http3 Request] GET ${req.url}'); + var _sent = client.send(req); + if(!Settings.ignoreHTTPTimeout && timeout != null){ + _sent.timeout(timeout); + } + res = await _sent; + Logger.info('[Http3 Request] GET ${req.url} code: ${res.statusCode}'); + if(((res.statusCode) / 100).floor() != 3){ + break; + } + } while(followRedirects != false); + return res; + } + + Future post( + String url,{ + Map? headers, + String? body, + Duration? timeout, + bool? followRedirects = true, + } + ) async { + flutter_curl.Client client = await getClient(); + flutter_curl.Response? res; + do { + flutter_curl.Request req = flutter_curl.Request( + method: 'POST', + url: res?.headers['location'] ?? url, + headers: headers ?? {}, + body: (body?.isNotEmpty ?? false) ? flutter_curl.RequestBody.string(body ?? '') : null + ); + if(res?.headers['location']?.isNotEmpty ?? false){ + req.headers.addAll({'Referer': req.url}); + } + Logger.info('[Http3 Request] POST ${req.url}'); + var _sent = client.send(req); + if(!Settings.ignoreHTTPTimeout && timeout != null){ + _sent.timeout(timeout); + } + res = await _sent; + Logger.info('[Http3 Request] POST ${req.url} code: ${res.statusCode}'); + if(((res.statusCode) / 100).floor() != 3){ + break; + } + } while(followRedirects != false); + return res; + } + + static const libraryAbis = [ + 'arm64-v8a', + 'armeabi-v7a', + 'x86_64', + ]; + + Future _checkSharedLibraryAndroid(String soName) async { + if (!Platform.isAndroid) { + return null; + } + + final devicePlugin = DeviceInfoPlugin(); + final deviceInfo = await devicePlugin.androidInfo; + + final targetAbi = deviceInfo.supportedAbis.firstWhere( + (abi) => libraryAbis.contains(abi), + ); + if(!libraryAbis.contains(targetAbi)){ + return null; + } + final sharedLibraryPath = 'assets/libcurl/android/$targetAbi/${soName}.so'; + final sharedLibraryContent = await rootBundle.load(sharedLibraryPath); + + final tempDir = await getTemporaryDirectory(); + final libraryFile = File('${tempDir.path}/${soName}.so'); + if(await libraryFile.exists()){ + return libraryFile.path; + } + final createdFile = await libraryFile.create(); + final openFile = await createdFile.open(mode: FileMode.write); + final writtenFile = + await openFile.writeFrom(Uint8List.view(sharedLibraryContent.buffer)); + await writtenFile.close(); + + return libraryFile.path; + } + + static Future toHttpResponse(flutter_curl.Response res) async { + return http.Response( + String.fromCharCodes(res.body), + res.statusCode, + request: http.Request( + res.request?.method ?? '', + Uri.parse(res.request?.url ?? '') + ), + headers: res.headers, + isRedirect: (res.statusCode / 100).floor() == 3, + ); + } + + static Future toStreamedHttpResponse(flutter_curl.Response res) async { + return http.StreamedResponse( + Stream.value(res.body), + res.statusCode, + contentLength: res.body.length, + request: http.Request( + res.request?.method ?? '', + Uri.parse(res.request?.url ?? '') + ), + headers: res.headers, + isRedirect: (res.statusCode / 100).floor() == 3, + ); + } + + static Future fromHttpRequest(http.Request req) async { + return flutter_curl.Request( + method: req.method, + url: req.url.toString(), + headers: req.headers, + body: flutter_curl.RequestBody.string(req.body), + verbose: false, + verifySSL: true, + httpVersions: [flutter_curl.HTTPVersion.http3] + ); + } +} diff --git a/lib/network/wrapper.dart b/lib/network/wrapper.dart index c0ffc8cf6..cf8a8d67f 100644 --- a/lib/network/wrapper.dart +++ b/lib/network/wrapper.dart @@ -3,11 +3,16 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; +import 'package:flutter_curl/flutter_curl.dart' as flutter_curl; +import 'package:path_provider/path_provider.dart'; import 'package:violet/component/proxy/proxy.myvipwebtools.com.dart'; import 'package:violet/log/log.dart'; +import 'package:violet/network/libcurl.dart'; import 'package:violet/settings/settings.dart'; import 'package:violet/thread/semaphore.dart'; @@ -97,7 +102,16 @@ Future _ehentaiGet(String url, StreamedResponse response; try { - var _sent = client.send(request); + late var _sent; + if(Settings.useHttp3){ + try { + _sent = Http3Request().get(url, headers: headers); + } catch(_){ + _sent = client.send(request); + } + } else { + _sent = client.send(request); + } if(!Settings.ignoreHTTPTimeout){ _sent.timeout( const Duration(seconds: 3), @@ -107,7 +121,12 @@ Future _ehentaiGet(String url, }, ); } - response = await _sent; + var _res = await _sent; + if(_res is flutter_curl.Response){ + response = await Http3Request.toStreamedHttpResponse(_res); + } else { + response = _res; + } } catch (e, st) { Logger.error('[Http Request] GET: $url\n' 'E:$e\n' @@ -163,13 +182,30 @@ Future _scriptGet(String url, var _headers = headers ?? {}; _headers['user-agent'] = HttpWrapper.userAgent; if (timeout == null) { + if(Settings.useHttp3){ + try { + var _res = await Http3Request().get(url, headers: _headers); + res = await Http3Request.toHttpResponse(_res); + } catch(_){ + res = await http.get(Uri.parse(url), headers: _headers); + } + } res = await http.get(Uri.parse(url), headers: _headers); } else { bool isTimeout = false; var retry = 0; do { isTimeout = false; - var _sent = http.get(Uri.parse(url), headers: _headers); + late var _sent; + if(Settings.useHttp3){ + try { + _sent = Http3Request().get(url, headers: _headers); + } catch(_){ + _sent = http.get(Uri.parse(url), headers: _headers); + } + } else { + _sent = http.get(Uri.parse(url), headers: _headers); + } if(!Settings.ignoreHTTPTimeout){ _sent .timeout( @@ -181,7 +217,12 @@ Future _scriptGet(String url, }, ); } - res = await _sent; + var _res = await _sent; + if(_res is flutter_curl.Response){ + res = await Http3Request.toHttpResponse(_res); + } else { + res = _res; + } } while (isTimeout && retry < 10); } diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 25a508dc6..f8e8f2696 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -1492,6 +1492,38 @@ class _SettingsPageState extends State }); }, ), + _buildDivider(), + InkWell( + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(8.0), + bottomRight: Radius.circular(8.0))), + child: ListTile( + leading: Image.asset( + 'assets/images/logo.png', + width: 25, + height: 25, + ), + title: Text(Translations.of(context).trans('usehttp3')), + trailing: Switch( + value: Settings.useHttp3, + onChanged: (newValue) async { + await Settings.setUseHttp3(newValue); + setState(() { + _shouldReload = true; + }); + }, + activeTrackColor: Settings.majorColor, + activeColor: Settings.majorAccentColor, + ), + ), + onTap: () async { + await Settings.setUseHttp3(!Settings.useHttp3); + setState(() { + _shouldReload = true; + }); + }, + ), // _buildDivider(), // InkWell( // customBorder: const RoundedRectangleBorder( diff --git a/lib/settings/settings.dart b/lib/settings/settings.dart index 05eed7e61..22a162181 100644 --- a/lib/settings/settings.dart +++ b/lib/settings/settings.dart @@ -28,6 +28,7 @@ class Settings { // Http Settings static late bool ignoreHTTPTimeout; // default false + static late bool useHttp3; // default false // Color Settings static late Color themeColor; // default light @@ -212,6 +213,7 @@ class Settings { deleteOldLogAtStart = await _getBool('deleteoldlogatstart'); ignoreHTTPTimeout = await _getBool('ignorehttptimeout'); + useHttp3 = await _getBool('usehttp3'); rightToLeft = await _getBool('right2left', true); isHorizontal = await _getBool('ishorizontal'); @@ -491,6 +493,12 @@ class Settings { await prefs.setBool('ignorehttptimeout', nn); } + static Future setUseHttp3(bool nn) async { + useHttp3 = nn; + + await prefs.setBool('usehttp3', nn); + } + static Future setSearchResultType(int wh) async { searchResultType = wh; diff --git a/pubspec.yaml b/pubspec.yaml index 489db5eb4..a5b42502d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -124,6 +124,7 @@ dependencies: webview_flutter: ^3.0.0 collection: ^1.16.0 octo_image: ^2.0.0 + flutter_curl: ^0.3.0 dev_dependencies: flutter_test: @@ -182,6 +183,16 @@ flutter: - assets/p7zip/x86/lib7zr.so # @dependent: android - assets/p7zip/x86_64/lib7zr.so # @dependent: android + - assets/libcurl/android/arm64-v8a/libcurl.so # @dependent: android + - assets/libcurl/android/armeabi-v7a/libcurl.so # @dependent: android + # - assets/libcurl/android/x86/libcurl.so # @dependent: android + - assets/libcurl/android/x86_64/libcurl.so # @dependent: android + + - assets/libcurl/android/arm64-v8a/libquiche.so # @dependent: android + - assets/libcurl/android/armeabi-v7a/libquiche.so # @dependent: android + # - assets/libcurl/android/x86/libquiche.so # @dependent: android + - assets/libcurl/android/x86_64/libquiche.so # @dependent: android + - assets/webview/ # An image asset can refer to one or more resolution-specific "variants", see