diff --git a/lib/grpc/dm.dart b/lib/grpc/dm.dart index 8cc73eaf8..5c5c1f447 100644 --- a/lib/grpc/dm.dart +++ b/lib/grpc/dm.dart @@ -18,6 +18,7 @@ abstract final class DmGrpc { type: type, ), DmSegMobileReply.fromBuffer, + isolate: true, ); } } diff --git a/lib/grpc/grpc_req.dart b/lib/grpc/grpc_req.dart index 8cb331cf6..c4a56b7d4 100644 --- a/lib/grpc/grpc_req.dart +++ b/lib/grpc/grpc_req.dart @@ -1,120 +1,19 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/grpc/bilibili/metadata.pb.dart'; -import 'package:PiliPlus/grpc/bilibili/metadata/device.pb.dart'; -import 'package:PiliPlus/grpc/bilibili/metadata/fawkes.pb.dart'; -import 'package:PiliPlus/grpc/bilibili/metadata/locale.pb.dart'; -import 'package:PiliPlus/grpc/bilibili/metadata/network.pb.dart' as network; import 'package:PiliPlus/grpc/bilibili/rpc.pb.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/id_utils.dart'; -import 'package:PiliPlus/utils/login_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:archive/archive.dart'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/foundation.dart' show kDebugMode, compute; import 'package:protobuf/protobuf.dart' show GeneratedMessage; abstract final class GrpcReq { - static String? _accessKey = Accounts.main.accessKey; - static const _build = 2001100; - static const _versionName = '2.0.1'; - static const _biliChannel = 'master'; - static const _mobiApp = 'android_hd'; - static const _device = 'android'; + static const _isolateSize = 256 * 1024; - static final _buvid = LoginUtils.buvid; - static final _traceId = IdUtils.genTraceId(); - static final _sessionId = Utils.generateRandomString(8); - - static void updateHeaders(String? accessKey) { - _accessKey = accessKey; - if (_accessKey != null) { - headers['authorization'] = 'identify_v1 $_accessKey'; - } else { - headers.remove('authorization'); - } - headers['x-bili-metadata-bin'] = base64Encode( - Metadata( - accessKey: _accessKey ?? '', - mobiApp: _mobiApp, - device: _device, - build: _build, - channel: _biliChannel, - buvid: _buvid, - platform: _device, - ).writeToBuffer(), - ); - } - - static final Map headers = { - Headers.contentTypeHeader: 'application/grpc', - 'grpc-encoding': 'gzip', - 'gzip-accept-encoding': 'gzip,identity', - 'user-agent': Constants.userAgent, - 'x-bili-gaia-vtoken': '', - 'x-bili-aurora-zone': '', - 'x-bili-trace-id': _traceId, - if (_accessKey != null) 'authorization': 'identify_v1 $_accessKey', - 'buvid': _buvid, - 'bili-http-engine': 'cronet', - 'te': 'trailers', - 'x-bili-fawkes-req-bin': base64Encode( - FawkesReq( - appkey: _mobiApp, - env: 'prod', - sessionId: _sessionId, - ).writeToBuffer(), - ), - 'x-bili-metadata-bin': base64Encode( - Metadata( - accessKey: _accessKey ?? '', - mobiApp: _mobiApp, - device: _device, - build: _build, - channel: _biliChannel, - buvid: _buvid, - platform: _device, - ).writeToBuffer(), - ), - 'x-bili-device-bin': base64Encode( - Device( - appId: 5, - build: _build, - buvid: _buvid, - mobiApp: _mobiApp, - platform: _device, - channel: _biliChannel, - brand: _device, - model: _device, - osver: '15', - versionName: _versionName, - ).writeToBuffer(), - ), - 'x-bili-network-bin': base64Encode( - network.Network( - type: network.NetworkType.WIFI, - ).writeToBuffer(), - ), - 'x-bili-locale-bin': base64Encode( - Locale( - cLocale: LocaleIds(language: 'zh', region: 'CN', script: 'Hans'), - sLocale: LocaleIds(language: 'zh', region: 'CN', script: 'Hans'), - timezone: 'Asia/Shanghai', - ).writeToBuffer(), - ), - 'x-bili-exps-bin': '', - }; - - static final Options options = Options( - headers: headers, - responseType: ResponseType.bytes, - ); + static final options = Options(responseType: ResponseType.bytes); static Uint8List compressProtobuf(Uint8List proto) { proto = const GZipEncoder().encodeBytes(proto); @@ -136,11 +35,22 @@ abstract final class GrpcReq { } } + static LoadingState _parse((Uint8List, T Function(Uint8List)) args) { + try { + final data = decompressProtobuf(args.$1); + final grpcResponse = args.$2(data); + return Success(grpcResponse); + } catch (e) { + return Error(e.toString()); + } + } + static Future> request( String url, GeneratedMessage request, - T Function(Uint8List) grpcParser, - ) async { + T Function(Uint8List) grpcParser, { + bool isolate = false, + }) async { final response = await Request().post( HttpString.appBaseUrl + url, data: compressProtobuf(request.writeToBuffer()), @@ -152,13 +62,13 @@ abstract final class GrpcReq { } if (response.headers.value('Grpc-Status') == '0') { - try { - Uint8List data = response.data; - data = decompressProtobuf(data); - final grpcResponse = grpcParser(data); - return Success(grpcResponse); - } catch (e) { - return Error(e.toString()); + final data = response.data; + if (data is Uint8List) { + return isolate && data.length > _isolateSize + ? compute(_parse, (data, grpcParser)) + : _parse((data, grpcParser)); + } else { + return Error('grpc: ${data.runtimeType} is not Uint8List'); } } else { try { diff --git a/lib/pages/setting/models/privacy_settings.dart b/lib/pages/setting/models/privacy_settings.dart index 387904bbd..9ede6c1a3 100644 --- a/lib/pages/setting/models/privacy_settings.dart +++ b/lib/pages/setting/models/privacy_settings.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/setting/models/model.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/accounts/account_manager/account_mgr.dart'; +import 'package:PiliPlus/utils/accounts/api_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -61,7 +61,7 @@ Widget _getAccountDetail(BuildContext context) { final slivers = []; final theme = TextTheme.of(context); for (var i in AccountType.values) { - final url = AccountManager.apiTypeSet[i]; + final url = ApiType.apiTypeSet[i]; if (url == null) continue; slivers diff --git a/lib/utils/accounts/account.dart b/lib/utils/accounts/account.dart index b2922c30f..6e6cabdeb 100644 --- a/lib/utils/accounts/account.dart +++ b/lib/utils/accounts/account.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/accounts/grpc_headers.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:hive/hive.dart'; @@ -26,6 +27,8 @@ sealed class Account { Map get headers => throw UnimplementedError(); + Map get grpcHeaders => throw UnimplementedError(); + bool get isLogin => throw UnimplementedError(); int get mid => throw UnimplementedError(); @@ -65,6 +68,11 @@ class LoginAccount extends Account { 'x-bili-aurora-eid': IdUtils.genAuroraEid(mid), }; + @override + late final Map grpcHeaders = GrpcHeaders.newHeaders( + accessKey, + ); + @override late final String csrf = cookieJar.domainCookies['bilibili.com']!['/']!['bili_jct']!.cookie.value; @@ -140,12 +148,17 @@ class AnonymousAccount extends Account { @override final Map headers = Constants.baseHeaders; + @override + final Map grpcHeaders = GrpcHeaders.newHeaders(); + @override bool activated = false; @override - Future delete() => - cookieJar.deleteAll().whenComplete(cookieJar.setBuvid3); + Future delete() { + grpcHeaders['x-bili-fawkes-req-bin'] = GrpcHeaders.fawkes; + return cookieJar.deleteAll().whenComplete(cookieJar.setBuvid3); + } static final _instance = AnonymousAccount._(); diff --git a/lib/utils/accounts/account_manager/account_mgr.dart b/lib/utils/accounts/account_manager/account_mgr.dart index aac2bf39d..dbc76c00e 100644 --- a/lib/utils/accounts/account_manager/account_mgr.dart +++ b/lib/utils/accounts/account_manager/account_mgr.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; +import 'package:PiliPlus/utils/accounts/api_type.dart'; import 'package:PiliPlus/utils/app_sign.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; @@ -20,115 +21,6 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; final _setCookieReg = RegExp('(?<=)(,)(?=[^;]+?=)'); class AccountManager extends Interceptor { - static const Map> apiTypeSet = { - AccountType.heartbeat: { - Api.videoIntro, - Api.replyList, - Api.replyReplyList, - - // history - Api.heartBeat, - Api.historyReport, - Api.roomEntryAction, - Api.liveLikeReport, - Api.mediaListHistory, - // Api.historyList, - // Api.pauseHistory, - // Api.clearHistory, - // Api.delHistory, - // Api.searchHistory, - // Api.historyStatus, - // progress - Api.pgcInfo, - Api.pugvInfo, - - Api.ab2c, - Api.liveRoomInfo, - Api.liveRoomInfoH5, - Api.onlineTotal, - Api.dynamicDetail, - Api.aiConclusion, - Api.getSeasonDetailApi, - Api.liveRoomDmToken, - Api.liveRoomDmPrefetch, - Api.superChatMsg, - Api.searchByType, - Api.dynSearch, - Api.searchArchive, - - // Api.memberInfo, - // Api.bgmDetail, - // Api.space, - // Api.spaceAudio, - // Api.spaceComic, - // Api.spaceArchive, - // Api.spaceChargingArchive, - // Api.spaceSeason, - // Api.spaceSeries, - // Api.spaceBangumi, - // Api.spaceOpus, - // Api.spaceFav, - // Api.seasonSeries, - // Api.matchInfo, - // Api.articleList, - // Api.opusDetail, - // Api.articleView, - // Api.articleInfo, - }, - AccountType.recommend: { - Api.recommendListWeb, - Api.recommendListApp, - Api.feedDislike, - Api.feedDislikeCancel, - Api.hotList, - Api.relatedList, - Api.hotSearchList, // 不同账号搜索结果可能不一样 - Api.searchDefault, - Api.searchSuggest, - Api.liveList, - Api.searchTrending, - Api.searchRecommend, - Api.getRankApi, - Api.pgcRank, - Api.pgcSeasonRank, - Api.pgcIndexResult, - Api.popularSeriesOne, - Api.popularSeriesList, - Api.popularPrecious, - Api.liveAreaList, - Api.liveFeedIndex, - Api.liveSecondList, - Api.liveRoomAreaList, - Api.liveSearch, - Api.bgmRecommend, - Api.dynTopicRcmd, - Api.topicFeed, - Api.topicTop, - }, - // progress - AccountType.video: { - Api.ugcUrl, - Api.pgcUrl, - Api.pugvUrl, - Api.tvPlayUrl, - }, - }; - - static const loginApi = { - Api.getTVCode, - Api.qrcodePoll, - Api.getCaptcha, - Api.getWebKey, - Api.appSmsCode, - Api.loginByPwdApi, - Api.logInByAppSms, - Api.safeCenterGetInfo, - Api.preCapture, - Api.safeCenterSmsCode, - Api.safeCenterSmsVerify, - Api.oauth2AccessToken, - }; - AccountManager(); String blockServer = Pref.blockServer; @@ -164,29 +56,31 @@ class AccountManager extends Interceptor { ); } + final isApp = path.startsWith(HttpString.appBaseUrl); + + if (isApp && options.responseType == ResponseType.bytes) { + options.headers.addAll(account.grpcHeaders); + return handler.next(options); + } + options.headers ..addAll(account.headers) ..['referer'] ??= HttpString.baseUrl; // app端不需要管理cookie - if (path.startsWith(HttpString.appBaseUrl)) { + if (isApp) { // if (kDebugMode) debugPrint('is app: ${options.path}'); - // bytes是grpc响应 - if (options.responseType != ResponseType.bytes) { - final dataPtr = - (options.method == 'POST' && options.data is Map - ? options.data as Map - : options.queryParameters) - .cast(); - if (dataPtr.isNotEmpty) { - if (!account.accessKey.isNullOrEmpty) { - dataPtr['access_key'] = account.accessKey!; - } - dataPtr['ts'] ??= (DateTime.now().millisecondsSinceEpoch ~/ 1000) - .toString(); - AppSign.appSign(dataPtr); - // if (kDebugMode) debugPrint(dataPtr.toString()); + final dataPtr = (options.method == 'POST' && options.data is Map + ? (options.data as Map).cast() + : options.queryParameters); + if (dataPtr.isNotEmpty) { + if (!account.accessKey.isNullOrEmpty) { + dataPtr['access_key'] = account.accessKey!; } + dataPtr['ts'] ??= (DateTime.now().millisecondsSinceEpoch ~/ 1000) + .toString(); + AppSign.appSign(dataPtr); + // if (kDebugMode) debugPrint(dataPtr.toString()); } return handler.next(options); } else { @@ -222,9 +116,9 @@ class AccountManager extends Interceptor { void onResponse(Response response, ResponseInterceptorHandler handler) { final options = response.requestOptions; final path = options.path; - if (path.startsWith(HttpString.appBaseUrl) || - _skipCookie(path) || - options.extra['account'] is NoAccount) { + if (options.extra['account'] is NoAccount || + path.startsWith(HttpString.appBaseUrl) || + _skipCookie(path)) { return handler.next(response); } else { final future = _saveCookies( @@ -309,7 +203,7 @@ class AccountManager extends Interceptor { .map(Cookie.fromSetCookieValue) .toList(); final statusCode = response.statusCode ?? 0; - final locations = response.headers[HttpHeaders.locationHeader] ?? []; + final locations = response.headers[HttpHeaders.locationHeader] ?? const []; final isRedirectRequest = statusCode >= 300 && statusCode < 400; final originalUri = response.requestOptions.uri; final realUri = originalUri.resolveUri(response.realUri); @@ -335,11 +229,11 @@ class AccountManager extends Interceptor { path.contains('biliimg.com'); } - Account _findAccount(String path) => loginApi.contains(path) + Account _findAccount(String path) => ApiType.loginApi.contains(path) ? AnonymousAccount() : Accounts.get( AccountType.values.firstWhere( - (i) => apiTypeSet[i]?.contains(path) == true, + (i) => ApiType.apiTypeSet[i]?.contains(path) == true, orElse: () => AccountType.main, ), ); diff --git a/lib/utils/accounts/api_type.dart b/lib/utils/accounts/api_type.dart new file mode 100644 index 000000000..e98b6eb1c --- /dev/null +++ b/lib/utils/accounts/api_type.dart @@ -0,0 +1,114 @@ +import 'package:PiliPlus/http/api.dart'; +import 'package:PiliPlus/models/common/account_type.dart'; + +abstract final class ApiType { + // TODO: grpc api type + static const Map> apiTypeSet = { + AccountType.heartbeat: { + Api.videoIntro, + Api.replyList, + Api.replyReplyList, + + // history + Api.heartBeat, + Api.historyReport, + Api.roomEntryAction, + Api.liveLikeReport, + Api.mediaListHistory, + // Api.historyList, + // Api.pauseHistory, + // Api.clearHistory, + // Api.delHistory, + // Api.searchHistory, + // Api.historyStatus, + // progress + Api.pgcInfo, + Api.pugvInfo, + + Api.ab2c, + Api.liveRoomInfo, + Api.liveRoomInfoH5, + Api.onlineTotal, + Api.dynamicDetail, + Api.aiConclusion, + Api.getSeasonDetailApi, + Api.liveRoomDmToken, + Api.liveRoomDmPrefetch, + Api.superChatMsg, + Api.searchByType, + Api.dynSearch, + Api.searchArchive, + + // Api.memberInfo, + // Api.bgmDetail, + // Api.space, + // Api.spaceAudio, + // Api.spaceComic, + // Api.spaceArchive, + // Api.spaceChargingArchive, + // Api.spaceSeason, + // Api.spaceSeries, + // Api.spaceBangumi, + // Api.spaceOpus, + // Api.spaceFav, + // Api.seasonSeries, + // Api.matchInfo, + // Api.articleList, + // Api.opusDetail, + // Api.articleView, + // Api.articleInfo, + }, + AccountType.recommend: { + Api.recommendListWeb, + Api.recommendListApp, + Api.feedDislike, + Api.feedDislikeCancel, + Api.hotList, + Api.relatedList, + Api.hotSearchList, // 不同账号搜索结果可能不一样 + Api.searchDefault, + Api.searchSuggest, + Api.liveList, + Api.searchTrending, + Api.searchRecommend, + Api.getRankApi, + Api.pgcRank, + Api.pgcSeasonRank, + Api.pgcIndexResult, + Api.popularSeriesOne, + Api.popularSeriesList, + Api.popularPrecious, + Api.liveAreaList, + Api.liveFeedIndex, + Api.liveSecondList, + Api.liveRoomAreaList, + Api.liveSearch, + Api.bgmRecommend, + Api.dynTopicRcmd, + Api.topicFeed, + Api.topicTop, + }, + // progress + AccountType.video: { + Api.ugcUrl, + Api.pgcUrl, + Api.pugvUrl, + Api.tvPlayUrl, + }, + }; + + static const loginApi = { + Api.getTVCode, + Api.qrcodePoll, + Api.getCaptcha, + Api.getWebKey, + Api.appSmsCode, + Api.loginByPwdApi, + Api.logInByAppSms, + Api.safeCenterGetInfo, + Api.preCapture, + Api.safeCenterSmsCode, + Api.safeCenterSmsVerify, + Api.oauth2AccessToken, + }; +} diff --git a/lib/utils/accounts/grpc_headers.dart b/lib/utils/accounts/grpc_headers.dart new file mode 100644 index 000000000..41cd30691 --- /dev/null +++ b/lib/utils/accounts/grpc_headers.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/grpc/bilibili/metadata.pb.dart'; +import 'package:PiliPlus/grpc/bilibili/metadata/device.pb.dart'; +import 'package:PiliPlus/grpc/bilibili/metadata/fawkes.pb.dart'; +import 'package:PiliPlus/grpc/bilibili/metadata/locale.pb.dart'; +import 'package:PiliPlus/grpc/bilibili/metadata/network.pb.dart' as network; +import 'package:PiliPlus/utils/login_utils.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:dio/dio.dart'; + +abstract final class GrpcHeaders { + static const _build = 2001100; + static const _versionName = '2.0.1'; + static const _biliChannel = 'master'; + static const _mobiApp = 'android_hd'; + static const _device = 'android'; + + static String get _buvid => LoginUtils.buvid; + static String get _traceId => Constants.traceId; + static String get _sessionId => Utils.generateRandomString(8); + + static final Map _base = { + Headers.contentTypeHeader: 'application/grpc', + 'grpc-encoding': 'gzip', + 'gzip-accept-encoding': 'gzip,identity', + 'user-agent': Constants.userAgent, + 'x-bili-gaia-vtoken': '', + 'x-bili-aurora-zone': '', + 'x-bili-trace-id': _traceId, + 'buvid': _buvid, + 'bili-http-engine': 'cronet', + // 'te': 'trailers', // dio not supported + 'x-bili-device-bin': base64Encode( + Device( + appId: 5, + build: _build, + buvid: _buvid, + mobiApp: _mobiApp, + platform: _device, + channel: _biliChannel, + brand: _device, + model: _device, + osver: '15', + versionName: _versionName, + ).writeToBuffer(), + ), + 'x-bili-network-bin': base64Encode( + network.Network(type: network.NetworkType.WIFI).writeToBuffer(), + ), + 'x-bili-locale-bin': base64Encode( + Locale( + cLocale: LocaleIds(language: 'zh', region: 'CN', script: 'Hans'), + sLocale: LocaleIds(language: 'zh', region: 'CN', script: 'Hans'), + timezone: 'Asia/Shanghai', + ).writeToBuffer(), + ), + 'x-bili-exps-bin': '', + }; + + static String get fawkes => base64Encode( + FawkesReq( + appkey: _mobiApp, + env: 'prod', + sessionId: _sessionId, + ).writeToBuffer(), + ); + + static Map newHeaders([String? accessKey]) { + return { + ..._base, + if (accessKey != null) 'authorization': 'identify_v1 $accessKey', + 'x-bili-fawkes-req-bin': fawkes, + 'x-bili-metadata-bin': base64Encode( + Metadata( + accessKey: accessKey, + mobiApp: _mobiApp, + device: _device, + build: _build, + channel: _biliChannel, + buvid: _buvid, + platform: _device, + ).writeToBuffer(), + ), + }; + } +} diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index 5166593ed..e33220221 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -96,19 +96,15 @@ abstract final class IdUtils { return base64Encoded; } + // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/grpc_api/readme.md#x-bili-trace-id-生成算法 static String genTraceId() { - String randomId = Utils.generateRandomString(32); + final randomTraceId = StringBuffer(Utils.generateRandomString(24)); - StringBuffer randomTraceId = StringBuffer(randomId.substring(0, 24)); + final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000) >> 8; - int ts = DateTime.now().millisecondsSinceEpoch ~/ 1000; - - for (int i = 2; i >= 0; i--) { - ts >>= 8; - randomTraceId.write((ts & 0xFF).toRadixString(16).padLeft(2, '0')); - } - - randomTraceId.write(randomId.substring(30, 32)); + randomTraceId + ..write((ts & 0xFFFFFF).toRadixString(16).padLeft(6, '0')) + ..write(Utils.generateRandomString(2)); return '${randomTraceId.toString()}:${randomTraceId.toString().substring(16, 32)}:0:0'; } diff --git a/lib/utils/login_utils.dart b/lib/utils/login_utils.dart index 34f6c9b71..a31e86016 100644 --- a/lib/utils/login_utils.dart +++ b/lib/utils/login_utils.dart @@ -1,7 +1,6 @@ import 'dart:async' show FutureOr; import 'dart:io' show Platform; -import 'package:PiliPlus/grpc/grpc_req.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/user/info.dart'; @@ -49,7 +48,6 @@ abstract final class LoginUtils { final account = Accounts.main; final result = await UserHttp.userInfo(); if (result.isSuccess) { - GrpcReq.updateHeaders(account.accessKey); setWebCookie(account); RequestUtils.syncHistoryStatus(); final UserInfoData data = result.data; @@ -83,8 +81,6 @@ abstract final class LoginUtils { ..face.value = '' ..isLogin.value = false; - GrpcReq.updateHeaders(null); - return Future.wait([ if (!Platform.isLinux) web.CookieManager.instance(