mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-19 22:22:05 +00:00
329 lines
10 KiB
Dart
329 lines
10 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:http_interceptor/http_interceptor.dart';
|
|
import 'package:mangayomi/eval/model/m_bridge.dart';
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'package:mangayomi/eval/model/m_source.dart';
|
|
import 'package:mangayomi/main.dart';
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart'
|
|
as flutter_inappwebview;
|
|
import 'package:mangayomi/models/settings.dart';
|
|
import 'package:http/io_client.dart';
|
|
import 'package:mangayomi/services/http/rhttp/src/model/settings.dart';
|
|
import 'package:mangayomi/utils/log/log.dart';
|
|
import 'package:mangayomi/services/http/rhttp/rhttp.dart' as rhttp;
|
|
|
|
class MClient {
|
|
MClient();
|
|
static final defaultClient = IOClient(HttpClient());
|
|
static final Map<rhttp.ClientSettings, Client> rhttpPool = {};
|
|
static Client httpClient({
|
|
Map<String, dynamic>? reqcopyWith,
|
|
rhttp.ClientSettings? settings,
|
|
}) {
|
|
if (!(reqcopyWith?["useDartHttpClient"] ?? false)) {
|
|
try {
|
|
settings ??= rhttp.ClientSettings(
|
|
throwOnStatusCode: false,
|
|
proxySettings: reqcopyWith?["noProxy"] ?? false
|
|
? const rhttp.ProxySettings.noProxy()
|
|
: null,
|
|
timeout: reqcopyWith?["timeout"] != null
|
|
? Duration(seconds: reqcopyWith?["timeout"])
|
|
: null,
|
|
timeoutSettings: TimeoutSettings(
|
|
connectTimeout: reqcopyWith?["connectTimeout"] != null
|
|
? Duration(seconds: reqcopyWith?["connectTimeout"])
|
|
: null,
|
|
),
|
|
tlsSettings: rhttp.TlsSettings(
|
|
verifyCertificates: reqcopyWith?["verifyCertificates"] ?? false,
|
|
),
|
|
);
|
|
return rhttpPool.putIfAbsent(settings, () {
|
|
return rhttp.RhttpCompatibleClient.createSync(settings: settings);
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
return defaultClient;
|
|
}
|
|
|
|
static InterceptedClient init({
|
|
MSource? source,
|
|
Map<String, dynamic>? reqcopyWith,
|
|
rhttp.ClientSettings? settings,
|
|
bool showCloudFlareError = true,
|
|
}) {
|
|
final clientSettings = customDns == null || customDns!.trim().isEmpty
|
|
? settings
|
|
: settings?.copyWith(
|
|
dnsSettings: DnsSettings.dynamic(
|
|
resolver: (host) async => [customDns!],
|
|
),
|
|
) ??
|
|
ClientSettings(
|
|
dnsSettings: DnsSettings.dynamic(
|
|
resolver: (host) async => [customDns!],
|
|
),
|
|
);
|
|
return InterceptedClient.build(
|
|
client: httpClient(settings: clientSettings, reqcopyWith: reqcopyWith),
|
|
retryPolicy: ResolveCloudFlareChallenge(showCloudFlareError),
|
|
interceptors: [
|
|
MCookieManager(reqcopyWith),
|
|
LoggerInterceptor(showCloudFlareError),
|
|
],
|
|
);
|
|
}
|
|
|
|
static Map<String, String> getCookiesPref(String url) {
|
|
final cookiesList = isar.settings.getSync(227)!.cookiesList ?? [];
|
|
if (cookiesList.isEmpty) return {};
|
|
final host = Uri.parse(url).host;
|
|
final cookies = cookiesList
|
|
.firstWhere(
|
|
(element) => element.host == host || host.contains(element.host!),
|
|
orElse: () => MCookie(cookie: ""),
|
|
)
|
|
.cookie!;
|
|
if (cookies.isEmpty) return {};
|
|
return {HttpHeaders.cookieHeader: cookies};
|
|
}
|
|
|
|
static Future<void> setCookie(
|
|
String url,
|
|
String ua,
|
|
flutter_inappwebview.InAppWebViewController? webViewController, {
|
|
String? cookie,
|
|
}) async {
|
|
List<String> cookies = [];
|
|
// if incoming cookie is not empty, use it first
|
|
if (cookie != null && cookie.isNotEmpty) {
|
|
cookies = cookie
|
|
.split(RegExp('(?<=)(,)(?=[^;]+?=)'))
|
|
.where((cookie) => cookie.isNotEmpty)
|
|
.toList();
|
|
} else if (!Platform.isLinux) {
|
|
cookies =
|
|
(await flutter_inappwebview.CookieManager.instance(
|
|
webViewEnvironment: webViewEnvironment,
|
|
).getCookies(
|
|
url: flutter_inappwebview.WebUri(url),
|
|
webViewController: webViewController,
|
|
))
|
|
.map((e) => "${e.name}=${e.value}")
|
|
.toList();
|
|
}
|
|
if (cookies.isNotEmpty) {
|
|
final host = Uri.parse(url).host;
|
|
final newCookie = cookies.join("; ");
|
|
final settings = await isar.settings.get(227);
|
|
final existingCookies = settings!.cookiesList ?? [];
|
|
final filteredCookies = removeCookiesForHost(existingCookies, host);
|
|
filteredCookies.add(
|
|
MCookie()
|
|
..host = host
|
|
..cookie = newCookie,
|
|
);
|
|
await isar.writeTxn(
|
|
() => isar.settings.put(settings..cookiesList = filteredCookies),
|
|
);
|
|
}
|
|
if (ua.isNotEmpty) {
|
|
final settings = await isar.settings.get(227);
|
|
await isar.writeTxn(
|
|
() => isar.settings.put(
|
|
settings!
|
|
..userAgent = ua
|
|
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
static List<MCookie> removeCookiesForHost(
|
|
List<MCookie> allCookies,
|
|
String host,
|
|
) {
|
|
return allCookies
|
|
.where((cookie) => cookie.host != host && !host.contains(cookie.host!))
|
|
.toList();
|
|
}
|
|
|
|
static Future<void> deleteAllCookies(String url) async {
|
|
final settings = await isar.settings.get(227);
|
|
final oldCookies = settings!.cookiesList ?? [];
|
|
final host = Uri.parse(url).host;
|
|
settings.cookiesList = removeCookiesForHost(oldCookies, host);
|
|
await isar.writeTxn(() => isar.settings.put(settings));
|
|
}
|
|
}
|
|
|
|
class MCookieManager extends InterceptorContract {
|
|
MCookieManager(this.reqcopyWith);
|
|
Map<String, dynamic>? reqcopyWith;
|
|
|
|
@override
|
|
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
|
final cookie = MClient.getCookiesPref(request.url.toString());
|
|
if (cookie.isNotEmpty) {
|
|
final settings = await isar.settings.get(227);
|
|
final userAgent = settings!.userAgent!;
|
|
if (request.headers[HttpHeaders.cookieHeader] == null) {
|
|
request.headers.addAll(cookie);
|
|
}
|
|
if (request.headers[HttpHeaders.userAgentHeader] == null) {
|
|
request.headers[HttpHeaders.userAgentHeader] = userAgent;
|
|
}
|
|
}
|
|
try {
|
|
if (reqcopyWith != null) {
|
|
if (reqcopyWith!["followRedirects"] != null) {
|
|
request.followRedirects = reqcopyWith!["followRedirects"];
|
|
}
|
|
if (reqcopyWith!["maxRedirects"] != null) {
|
|
request.maxRedirects = reqcopyWith!["maxRedirects"];
|
|
}
|
|
if (reqcopyWith!["contentLength"] != null) {
|
|
request.contentLength = reqcopyWith!["contentLength"];
|
|
}
|
|
if (reqcopyWith!["persistentConnection"] != null) {
|
|
request.persistentConnection = reqcopyWith!["persistentConnection"];
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
return request;
|
|
}
|
|
|
|
@override
|
|
Future<BaseResponse> interceptResponse({
|
|
required BaseResponse response,
|
|
}) async {
|
|
return response;
|
|
}
|
|
}
|
|
|
|
class LoggerInterceptor extends InterceptorContract {
|
|
LoggerInterceptor(this.showCloudFlareError);
|
|
bool showCloudFlareError;
|
|
@override
|
|
Future<BaseRequest> interceptRequest({required BaseRequest request}) async {
|
|
final content =
|
|
"----- Request -----\n${request.toString()}\nheaders: ${request.headers.toString()}";
|
|
if (kDebugMode) {
|
|
print(content);
|
|
}
|
|
if (useLogger) {
|
|
Logger.add(LoggerLevel.info, content);
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
@override
|
|
Future<BaseResponse> interceptResponse({
|
|
required BaseResponse response,
|
|
}) async {
|
|
if (showCloudFlareError) {
|
|
final cloudflare = isCloudflare(response);
|
|
final content =
|
|
"----- Response -----\n${response.request?.method}: ${response.request?.url}, statusCode: ${response.statusCode} ${cloudflare ? "Failed to bypass Cloudflare" : ""}";
|
|
if (kDebugMode) {
|
|
print(content);
|
|
}
|
|
if (useLogger) {
|
|
Logger.add(LoggerLevel.info, content);
|
|
}
|
|
if (cloudflare) {
|
|
botToast(
|
|
"${response.statusCode} Failed to bypass Cloudflare",
|
|
hasCloudFlare: cloudflare,
|
|
url: response.request!.url.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
|
|
bool isCloudflare(BaseResponse response) {
|
|
return [403, 503].contains(response.statusCode) &&
|
|
["cloudflare-nginx", "cloudflare"].contains(response.headers["server"]);
|
|
}
|
|
|
|
class ResolveCloudFlareChallenge extends RetryPolicy {
|
|
bool showCloudFlareError;
|
|
ResolveCloudFlareChallenge(this.showCloudFlareError);
|
|
@override
|
|
int get maxRetryAttempts => 2;
|
|
@override
|
|
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
|
|
if (!showCloudFlareError || Platform.isLinux) return false;
|
|
flutter_inappwebview.HeadlessInAppWebView? headlessWebView;
|
|
int time = 0;
|
|
bool timeOut = false;
|
|
bool isCloudFlare = isCloudflare(response);
|
|
if (isCloudFlare) {
|
|
headlessWebView = flutter_inappwebview.HeadlessInAppWebView(
|
|
webViewEnvironment: webViewEnvironment,
|
|
initialUrlRequest: flutter_inappwebview.URLRequest(
|
|
url: flutter_inappwebview.WebUri(response.request!.url.toString()),
|
|
),
|
|
onLoadStop: (controller, url) async {
|
|
try {
|
|
isCloudFlare = await controller.platform.evaluateJavascript(
|
|
source:
|
|
"document.head.innerHTML.includes('#challenge-success-text')",
|
|
);
|
|
} catch (_) {
|
|
isCloudFlare = false;
|
|
}
|
|
|
|
await Future.doWhile(() async {
|
|
if (!timeOut && isCloudFlare) {
|
|
try {
|
|
isCloudFlare = await controller.platform.evaluateJavascript(
|
|
source:
|
|
"document.head.innerHTML.includes('#challenge-success-text')",
|
|
);
|
|
} catch (_) {
|
|
isCloudFlare = false;
|
|
}
|
|
}
|
|
if (isCloudFlare) await Future.delayed(Duration(milliseconds: 300));
|
|
|
|
return isCloudFlare;
|
|
});
|
|
if (!timeOut) {
|
|
final ua =
|
|
await controller.evaluateJavascript(
|
|
source: "navigator.userAgent",
|
|
) ??
|
|
"";
|
|
await MClient.setCookie(url.toString(), ua, controller);
|
|
}
|
|
},
|
|
);
|
|
|
|
headlessWebView.run();
|
|
|
|
await Future.doWhile(() async {
|
|
timeOut = time == 15;
|
|
if (!isCloudFlare || timeOut) {
|
|
return false;
|
|
}
|
|
await Future.delayed(const Duration(seconds: 1));
|
|
time++;
|
|
return true;
|
|
});
|
|
try {
|
|
headlessWebView.dispose();
|
|
} catch (_) {}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|