import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:math' show Random;
import 'package:PiliPlus/build_config.dart';
import 'package:archive/archive.dart';
import 'package:brotli/brotli.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:flutter/material.dart';
import 'package:PiliPlus/utils/id_utils.dart';
import '../utils/storage.dart';
import '../utils/utils.dart';
import 'api.dart';
import 'constants.dart';
import 'interceptor.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as web;
class Request {
static const gzipDecoder = GZipDecoder();
static const brotilDecoder = BrotliDecoder();
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
late bool enableSystemProxy;
late String systemProxyHost;
late String systemProxyPort;
static final RegExp spmPrefixExp =
RegExp(r'');
/// 设置cookie
static setCookie() async {
final String cookiePath = await Utils.getCookiePath();
final PersistCookieJar cookieJar = PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(cookiePath),
);
cookieManager = CookieManager(cookieJar);
dio.interceptors.add(cookieManager);
dio.interceptors.add(ApiInterceptor());
final List cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
for (Cookie item in cookies) {
await web.CookieManager().setCookie(
url: web.WebUri(item.domain ?? ''),
name: item.name,
value: item.value,
path: item.path ?? '',
domain: item.domain,
isSecure: item.secure,
isHttpOnly: item.httpOnly,
);
}
final userInfo = GStorage.userInfo.get('userInfoCache');
if (userInfo?.mid != null) {
final List tUrlCookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (tUrlCookies.isEmpty) {
try {
await dio.head(HttpString.tUrl);
} catch (e) {
log("setCookie, ${e.toString()}");
}
}
setOptionsHeaders(userInfo);
}
try {
await buvidActivate();
} catch (e) {
log("setCookie, ${e.toString()}");
}
// final String cookieString = cookies
// .map((Cookie cookie) => '${cookie.name}=${cookie.value}')
// .join('; ');
// dio.options.headers['cookie'] = cookieString;
}
// 从cookie中获取 csrf token
static Future getCsrf() async {
List cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.apiBaseUrl));
return cookies
.firstWhere((e) => e.name == 'bili_jct', orElse: () => Cookie('', ''))
.value;
}
static setOptionsHeaders(userInfo) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
dio.options.headers['x-bili-aurora-eid'] =
IdUtils.genAuroraEid(userInfo.mid);
}
static Future buvidActivate() async {
var html = await Request().get(Api.dynamicSpmPrefix);
String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!;
Random rand = Random();
String randPngEnd = base64.encode(
List.generate(32, (_) => rand.nextInt(256)) +
List.filled(4, 0) +
[73, 69, 78, 68] +
List.generate(4, (_) => rand.nextInt(256)));
String jsonData = json.encode({
'3064': 1,
'39c8': '$spmPrefix.fp.risk',
'3c43': {
'adca': 'Linux',
'bfe9': randPngEnd.substring(randPngEnd.length - 50),
},
});
await Request().post(Api.activateBuvidApi,
data: {'payload': jsonData},
options: Options(contentType: Headers.jsonContentType));
}
/*
* config it and create
*/
Request._internal() {
//BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数
BaseOptions options = BaseOptions(
//请求基地址,可以包含子路径
baseUrl: HttpString.apiBaseUrl,
//连接服务器超时时间,单位是毫秒.
connectTimeout: const Duration(milliseconds: 12000),
//响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头.
headers: {
'connection': 'keep-alive',
'accept-encoding': 'br,gzip',
'user-agent': 'Dart/3.6 (dart:io)', // Http2Adapter不会自动添加标头
'referer': HttpString.baseUrl,
'env': 'prod',
'app-key': 'android64',
'x-bili-aurora-zone': 'sh001',
},
responseDecoder: responseDecoder, // Http2Adapter没有自动解压
persistentConnection: true);
enableSystemProxy = GStorage.setting
.get(SettingBoxKey.enableSystemProxy, defaultValue: false) as bool;
systemProxyHost =
GStorage.setting.get(SettingBoxKey.systemProxyHost, defaultValue: '');
systemProxyPort =
GStorage.setting.get(SettingBoxKey.systemProxyPort, defaultValue: '');
final http11Adapter = IOHttpClientAdapter(createHttpClient: () {
final client = HttpClient()
..idleTimeout = const Duration(seconds: 30)
..autoUncompress = false; // Http2Adapter没有自动解压, 统一行为
// 设置代理
if (enableSystemProxy) {
client.findProxy = (_) => 'PROXY $systemProxyHost:$systemProxyPort';
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
}
return client;
});
dio = Dio(options)
..httpClientAdapter =
GStorage.setting.get(SettingBoxKey.enableHttp2, defaultValue: false)
? Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(seconds: 30),
onClientCreate: (_, ClientSetting config) {
config.onBadCertificate = (_) => true;
if (enableSystemProxy) {
config.proxy = Uri(
scheme: 'http',
host: systemProxyHost,
port: int.parse(systemProxyPort));
}
}),
fallbackAdapter: http11Adapter)
: http11Adapter;
// 日志拦截器 输出请求、响应内容
if (BuildConfig.isDebug) {
dio.interceptors.add(LogInterceptor(
request: false,
requestHeader: false,
responseHeader: false,
));
}
dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (int? status) {
return status! >= 200 && status < 300;
};
}
/*
* get请求
*/
Future get(url,
{queryParameters, options, cancelToken, extra}) async {
Response response;
if (extra != null) {
if (extra['ua'] != null) {
options ??= Options();
options.headers ??= {};
options.headers!['user-agent'] = headerUa(type: extra['ua']);
}
}
try {
response = await dio.get(
url,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
return response;
} on DioException catch (e) {
Response errResponse = Response(
data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: -1,
requestOptions: RequestOptions(),
);
return errResponse;
}
}
/*
* post请求
*/
Future post(url,
{data, queryParameters, options, cancelToken, extra}) async {
// debugPrint('post-data: $data');
Response response;
try {
response = await dio.post(
url,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
// debugPrint('post success: ${response.data}');
return response;
} on DioException catch (e) {
Response errResponse = Response(
data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: -1,
requestOptions: RequestOptions(),
);
return errResponse;
}
}
/*
* 下载文件
*/
downloadFile(urlPath, savePath) async {
Response response;
try {
response = await dio.download(urlPath, savePath,
onReceiveProgress: (int count, int total) {
//进度
// debugPrint("$count $total");
});
debugPrint('downloadFile success: ${response.data}');
return response.data;
} on DioException catch (e) {
debugPrint('downloadFile error: $e');
return Future.error(ApiInterceptor.dioError(e));
}
}
/*
* 取消请求
*
* 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。
* 所以参数可选
*/
void cancelRequests(CancelToken token) {
token.cancel("cancelled");
}
static String headerUa({type = 'mob'}) {
return type == 'mob'
? Platform.isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1'
: 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36'
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
}
static String responseDecoder(List responseBytes, RequestOptions options,
ResponseBody responseBody) {
switch (responseBody.headers['content-encoding']?.firstOrNull) {
case 'gzip':
return utf8.decode(gzipDecoder.decodeBytes(responseBytes),
allowMalformed: true);
case 'br':
return utf8.decode(brotilDecoder.convert(responseBytes),
allowMalformed: true);
default:
return utf8.decode(responseBytes, allowMalformed: true);
}
}
}