initial commit

This commit is contained in:
nyne
2024-09-29 16:17:03 +08:00
commit f08c5cccb9
196 changed files with 16761 additions and 0 deletions

194
lib/network/app_dio.dart Normal file
View 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
View 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
View 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;
}