mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
initial commit
This commit is contained in:
194
lib/network/app_dio.dart
Normal file
194
lib/network/app_dio.dart
Normal file
@@ -0,0 +1,194 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
|
||||
import '../foundation/app.dart';
|
||||
|
||||
class MyLogInterceptor implements Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
Log.error("Network",
|
||||
"${err.requestOptions.method} ${err.requestOptions.path}\n$err\n${err.response?.data.toString()}");
|
||||
switch (err.type) {
|
||||
case DioExceptionType.badResponse:
|
||||
var statusCode = err.response?.statusCode;
|
||||
if (statusCode != null) {
|
||||
err = err.copyWith(
|
||||
message: "Invalid Status Code: $statusCode. "
|
||||
"${_getStatusCodeInfo(statusCode)}");
|
||||
}
|
||||
case DioExceptionType.connectionTimeout:
|
||||
err = err.copyWith(message: "Connection Timeout");
|
||||
case DioExceptionType.receiveTimeout:
|
||||
err = err.copyWith(
|
||||
message: "Receive Timeout: "
|
||||
"This indicates that the server is too busy to respond");
|
||||
case DioExceptionType.unknown:
|
||||
if (err.toString().contains("Connection terminated during handshake")) {
|
||||
err = err.copyWith(
|
||||
message: "Connection terminated during handshake: "
|
||||
"This may be caused by the firewall blocking the connection "
|
||||
"or your requests are too frequent.");
|
||||
} else if (err.toString().contains("Connection reset by peer")) {
|
||||
err = err.copyWith(
|
||||
message: "Connection reset by peer: "
|
||||
"The error is unrelated to app, please check your network.");
|
||||
}
|
||||
default:
|
||||
{}
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
static const errorMessages = <int, String>{
|
||||
400: "The Request is invalid.",
|
||||
401: "The Request is unauthorized.",
|
||||
403: "No permission to access the resource. Check your account or network.",
|
||||
404: "Not found.",
|
||||
429: "Too many requests. Please try again later.",
|
||||
};
|
||||
|
||||
String _getStatusCodeInfo(int? statusCode) {
|
||||
if (statusCode != null && statusCode >= 500) {
|
||||
return "This is server-side error, please try again later. "
|
||||
"Do not report this issue.";
|
||||
} else {
|
||||
return errorMessages[statusCode] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(
|
||||
Response<dynamic> response, ResponseInterceptorHandler handler) {
|
||||
var headers = response.headers.map.map((key, value) => MapEntry(
|
||||
key.toLowerCase(), value.length == 1 ? value.first : value.toString()));
|
||||
headers.remove("cookie");
|
||||
String content;
|
||||
if (response.data is List<int>) {
|
||||
try {
|
||||
content = utf8.decode(response.data, allowMalformed: false);
|
||||
} catch (e) {
|
||||
content = "<Bytes>\nlength:${response.data.length}";
|
||||
}
|
||||
} else {
|
||||
content = response.data.toString();
|
||||
}
|
||||
Log.addLog(
|
||||
(response.statusCode != null && response.statusCode! < 400)
|
||||
? LogLevel.info
|
||||
: LogLevel.error,
|
||||
"Network",
|
||||
"Response ${response.realUri.toString()} ${response.statusCode}\n"
|
||||
"headers:\n$headers\n$content");
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
options.connectTimeout = const Duration(seconds: 15);
|
||||
options.receiveTimeout = const Duration(seconds: 15);
|
||||
options.sendTimeout = const Duration(seconds: 15);
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
|
||||
class AppDio with DioMixin {
|
||||
String? _proxy = proxy;
|
||||
|
||||
AppDio(BaseOptions options) {
|
||||
this.options = options;
|
||||
interceptors.add(MyLogInterceptor());
|
||||
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient);
|
||||
}
|
||||
|
||||
static HttpClient createHttpClient() {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 5);
|
||||
client.findProxy = (uri) => proxy ?? "DIRECT";
|
||||
client.idleTimeout = const Duration(seconds: 100);
|
||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
|
||||
if (host.contains("cdn")) return true;
|
||||
final ipv4RegExp = RegExp(
|
||||
r'^((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$');
|
||||
if (ipv4RegExp.hasMatch(host)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return client;
|
||||
}
|
||||
|
||||
static String? proxy;
|
||||
|
||||
static Future<String?> getProxy() async {
|
||||
if (appdata.settings['proxy'].removeAllBlank == "direct") return null;
|
||||
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
|
||||
|
||||
String res;
|
||||
if (!App.isLinux) {
|
||||
const channel = MethodChannel("venera/method_channel");
|
||||
try {
|
||||
res = await channel.invokeMethod("getProxy");
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
res = "No Proxy";
|
||||
}
|
||||
if (res == "No Proxy") return null;
|
||||
|
||||
if (res.contains(";")) {
|
||||
var proxies = res.split(";");
|
||||
for (String proxy in proxies) {
|
||||
proxy = proxy.removeAllBlank;
|
||||
if (proxy.startsWith('https=')) {
|
||||
return proxy.substring(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final RegExp regex = RegExp(
|
||||
r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$',
|
||||
caseSensitive: false,
|
||||
multiLine: false,
|
||||
);
|
||||
if (!regex.hasMatch(res)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<T>> request<T> (
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
CancelToken? cancelToken,
|
||||
Options? options,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
proxy = await getProxy();
|
||||
if(_proxy != proxy) {
|
||||
_proxy = proxy;
|
||||
(httpClientAdapter as IOHttpClientAdapter).close();
|
||||
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient);
|
||||
}
|
||||
return super.request(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
cancelToken: cancelToken,
|
||||
options: options,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
}
|
221
lib/network/cookie_jar.dart
Normal file
221
lib/network/cookie_jar.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
|
||||
class CookieJarSql {
|
||||
late Database _db;
|
||||
|
||||
final String path;
|
||||
|
||||
CookieJarSql(this.path){
|
||||
init();
|
||||
}
|
||||
|
||||
void init() {
|
||||
_db = sqlite3.open(path);
|
||||
_db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS cookies (
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
path TEXT,
|
||||
expires INTEGER,
|
||||
secure INTEGER,
|
||||
httpOnly INTEGER,
|
||||
PRIMARY KEY (name, domain, path)
|
||||
);
|
||||
''');
|
||||
}
|
||||
|
||||
void saveFromResponse(Uri uri, List<Cookie> cookies) {
|
||||
var current = loadForRequest(uri);
|
||||
for (var cookie in cookies) {
|
||||
var currentCookie = current.firstWhereOrNull((element) =>
|
||||
element.name == cookie.name &&
|
||||
(cookie.path == null || cookie.path!.startsWith(element.path!)));
|
||||
if (currentCookie != null) {
|
||||
cookie.domain = currentCookie.domain;
|
||||
}
|
||||
_db.execute('''
|
||||
INSERT OR REPLACE INTO cookies (name, value, domain, path, expires, secure, httpOnly)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
''', [
|
||||
cookie.name,
|
||||
cookie.value,
|
||||
cookie.domain ?? uri.host,
|
||||
cookie.path ?? "/",
|
||||
cookie.expires?.millisecondsSinceEpoch,
|
||||
cookie.secure ? 1 : 0,
|
||||
cookie.httpOnly ? 1 : 0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
List<Cookie> _loadWithDomain(String domain) {
|
||||
var rows = _db.select('''
|
||||
SELECT name, value, domain, path, expires, secure, httpOnly
|
||||
FROM cookies
|
||||
WHERE domain = ?;
|
||||
''', [domain]);
|
||||
|
||||
return rows
|
||||
.map((row) => Cookie(
|
||||
row["name"] as String,
|
||||
row["value"] as String,
|
||||
)
|
||||
..domain = row["domain"] as String
|
||||
..path = row["path"] as String
|
||||
..expires = row["expires"] == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(row["expires"] as int)
|
||||
..secure = row["secure"] == 1
|
||||
..httpOnly = row["httpOnly"] == 1)
|
||||
.toList();
|
||||
}
|
||||
|
||||
List<String> _getAcceptedDomains(String host) {
|
||||
var acceptedDomains = <String>[host];
|
||||
var hostParts = host.split(".");
|
||||
for (var i = 0; i < hostParts.length - 1; i++) {
|
||||
acceptedDomains.add(".${hostParts.sublist(i).join(".")}");
|
||||
}
|
||||
return acceptedDomains;
|
||||
}
|
||||
|
||||
List<Cookie> loadForRequest(Uri uri) {
|
||||
// if uri.host is example.example.com, acceptedDomains will be [".example.example.com", ".example.com", "example.com"]
|
||||
var acceptedDomains = _getAcceptedDomains(uri.host);
|
||||
|
||||
var cookies = <Cookie>[];
|
||||
for (var domain in acceptedDomains) {
|
||||
cookies.addAll(_loadWithDomain(domain));
|
||||
}
|
||||
|
||||
// check expires
|
||||
var expires = cookies.where((cookie) =>
|
||||
cookie.expires != null && cookie.expires!.isBefore(DateTime.now()));
|
||||
for (var cookie in expires) {
|
||||
_db.execute('''
|
||||
DELETE FROM cookies
|
||||
WHERE name = ? AND domain = ? AND path = ?;
|
||||
''', [cookie.name, cookie.domain, cookie.path]);
|
||||
}
|
||||
|
||||
return cookies
|
||||
.where((element) =>
|
||||
!expires.contains(element) && _checkPathMatch(uri, element.path))
|
||||
.toList();
|
||||
}
|
||||
|
||||
bool _checkPathMatch(Uri uri, String? cookiePath) {
|
||||
if (cookiePath == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cookiePath == uri.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cookiePath == "/") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (cookiePath.endsWith("/")) {
|
||||
return uri.path.startsWith(cookiePath);
|
||||
}
|
||||
|
||||
return uri.path.startsWith(cookiePath);
|
||||
}
|
||||
|
||||
void saveFromResponseCookieHeader(Uri uri, List<String> cookieHeader) {
|
||||
var cookies = cookieHeader
|
||||
.map((header) => Cookie.fromSetCookieValue(header))
|
||||
.toList();
|
||||
saveFromResponse(uri, cookies);
|
||||
}
|
||||
|
||||
String loadForRequestCookieHeader(Uri uri) {
|
||||
var cookies = loadForRequest(uri);
|
||||
var map = <String, Cookie>{};
|
||||
for (var cookie in cookies) {
|
||||
if(map.containsKey(cookie.name)) {
|
||||
if(cookie.domain![0] != '.' && map[cookie.name]!.domain![0] == '.') {
|
||||
map[cookie.name] = cookie;
|
||||
} else if(cookie.domain!.length > map[cookie.name]!.domain!.length) {
|
||||
map[cookie.name] = cookie;
|
||||
}
|
||||
} else {
|
||||
map[cookie.name] = cookie;
|
||||
}
|
||||
}
|
||||
return map.entries.map((cookie) => "${cookie.value.name}=${cookie.value.value}").join("; ");
|
||||
}
|
||||
|
||||
void delete(Uri uri, String name) {
|
||||
var acceptedDomains = _getAcceptedDomains(uri.host);
|
||||
for (var domain in acceptedDomains) {
|
||||
_db.execute('''
|
||||
DELETE FROM cookies
|
||||
WHERE name = ? AND domain = ? AND path = ?;
|
||||
''', [name, domain, uri.path]);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteUri(Uri uri) {
|
||||
var acceptedDomains = _getAcceptedDomains(uri.host);
|
||||
for (var domain in acceptedDomains) {
|
||||
_db.execute('''
|
||||
DELETE FROM cookies
|
||||
WHERE domain = ?;
|
||||
''', [domain]);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteAll() {
|
||||
_db.execute('''
|
||||
DELETE FROM cookies;
|
||||
''');
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_db.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SingleInstanceCookieJar extends CookieJarSql {
|
||||
factory SingleInstanceCookieJar(String path) =>
|
||||
instance ??= SingleInstanceCookieJar._create(path);
|
||||
|
||||
SingleInstanceCookieJar._create(super.path);
|
||||
|
||||
static SingleInstanceCookieJar? instance;
|
||||
}
|
||||
|
||||
class CookieManagerSql extends Interceptor {
|
||||
final CookieJarSql cookieJar;
|
||||
|
||||
CookieManagerSql(this.cookieJar);
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
var cookies = cookieJar.loadForRequestCookieHeader(options.uri);
|
||||
if (cookies.isNotEmpty) {
|
||||
options.headers["cookie"] = cookies;
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
cookieJar.saveFromResponseCookieHeader(
|
||||
response.requestOptions.uri, response.headers["set-cookie"] ?? []);
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
23
lib/network/download.dart
Normal file
23
lib/network/download.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
||||
|
||||
abstract class DownloadTask with ChangeNotifier {
|
||||
int get current;
|
||||
|
||||
int get total;
|
||||
|
||||
double get progress => current / total;
|
||||
|
||||
bool get isComplete => current == total;
|
||||
|
||||
int get speed;
|
||||
|
||||
void cancel();
|
||||
|
||||
void pause();
|
||||
|
||||
void resume();
|
||||
|
||||
String get title;
|
||||
|
||||
String? get cover;
|
||||
}
|
Reference in New Issue
Block a user