feat: migrate away from dio

This commit is contained in:
Tien Do Nam
2024-11-04 21:50:04 +01:00
parent 82e2d8c0de
commit 73d8c6d9a9
11 changed files with 115 additions and 145 deletions
+6 -6
View File
@@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:common/api_route_builder.dart';
@@ -7,7 +6,6 @@ import 'package:common/constants.dart';
import 'package:common/isolate.dart';
import 'package:common/model/dto/file_dto.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/util/dio.dart';
import 'package:common/util/logger.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter/foundation.dart';
@@ -61,6 +59,8 @@ Future<RefenaContainer> preInit(List<String> args) async {
initLogger(args.contains('-v') || args.contains('--verbose') ? Level.ALL : Level.INFO);
MapperContainer.globals.use(const FileDtoMapper());
await Rhttp.init();
final dynamicColors = await getDynamicColors();
final persistenceService = await PersistenceService.initialize(
@@ -78,20 +78,20 @@ Future<RefenaContainer> preInit(List<String> args) async {
// Check if this app is already open and let it "show up".
// If this is the case, then exit the current instance.
final dio = createDio(const Duration(milliseconds: 100), persistenceService.getSecurityContext());
final client = createRhttpClient(const Duration(milliseconds: 100), persistenceService.getSecurityContext());
try {
await dio.post(
await client.post(
ApiRoute.show.targetRaw(
'127.0.0.1',
persistenceService.getPort(),
persistenceService.isHttps(),
peerProtocolVersion,
),
queryParameters: {
query: {
'token': persistenceService.getShowToken(),
},
data: jsonEncode({
body: HttpBody.json({
'args': args,
}),
);
-39
View File
@@ -1,39 +0,0 @@
import 'package:common/util/dio.dart';
import 'package:dio/dio.dart';
import 'package:localsend_app/provider/logging/http_logs_provider.dart';
import 'package:localsend_app/provider/security_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:refena_flutter/refena_flutter.dart';
class DioCollection {
final Dio discovery;
final Dio longLiving;
DioCollection({
required this.discovery,
required this.longLiving,
});
}
/// Provides a dio having a specific timeout.
/// Changes must be made in common/lib/src/isolate/child/dio_provider.dart also
final dioProvider = ViewProvider((ref) {
final securityContext = ref.watch(securityProvider);
final discoveryTimeout = ref.watch(settingsProvider.select((state) => state.discoveryTimeout));
return DioCollection(
discovery: createDio(Duration(milliseconds: discoveryTimeout), securityContext),
longLiving: createDio(
const Duration(days: 30),
securityContext,
interceptor: LogInterceptor(
requestHeader: false,
requestBody: true,
request: false,
responseHeader: false,
responseBody: true,
error: true,
logPrint: (log) => ref.notifier(httpLogsProvider).addLog(log.toString()),
),
),
);
});
+52
View File
@@ -0,0 +1,52 @@
import 'package:localsend_app/provider/logging/http_logs_provider.dart';
import 'package:localsend_app/provider/security_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/rhttp.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:rhttp/rhttp.dart';
class HttpClientCollection {
final RhttpClient discovery;
final RhttpClient longLiving;
HttpClientCollection({
required this.discovery,
required this.longLiving,
});
}
/// Provides a dio having a specific timeout.
/// Changes must be made in common/lib/src/isolate/child/dio_provider.dart also
final httpProvider = ViewProvider((ref) {
final securityContext = ref.watch(securityProvider);
final discoveryTimeout = ref.watch(settingsProvider.select((state) => state.discoveryTimeout));
return HttpClientCollection(
discovery: createRhttpClient(Duration(milliseconds: discoveryTimeout), securityContext),
longLiving: createRhttpClient(
const Duration(days: 30),
securityContext,
interceptor: SimpleInterceptor(
beforeRequest: (request) async {
ref.notifier(httpLogsProvider).addLog('HTTP Request: ${request.method} ${request.url}');
return Interceptor.next();
},
afterResponse: (response) async {
final body = switch (response) {
HttpTextResponse() => response.body,
HttpBytesResponse() => '<${response.body.length} bytes>',
HttpStreamResponse() => '<stream>',
};
ref
.notifier(httpLogsProvider)
.addLog('HTTP Response: ${response.request.method} ${response.request.url} returned ${response.statusCode} with body: $body');
return Interceptor.next();
},
onError: (exception) async {
ref.notifier(httpLogsProvider).addLog(exception.toString());
return Interceptor.next();
},
),
),
);
});
+33 -22
View File
@@ -14,7 +14,6 @@ import 'package:common/model/file_status.dart';
import 'package:common/model/file_type.dart';
import 'package:common/model/session_status.dart';
import 'package:common/util/sleep.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:localsend_app/model/cross_file.dart';
import 'package:localsend_app/model/send_mode.dart';
@@ -24,13 +23,14 @@ import 'package:localsend_app/pages/home_page.dart';
import 'package:localsend_app/pages/progress_page.dart';
import 'package:localsend_app/pages/send_page.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/http_provider.dart';
import 'package:localsend_app/provider/progress_provider.dart';
import 'package:localsend_app/provider/selection/selected_sending_files_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/widget/dialogs/pin_dialog.dart';
import 'package:logging/logging.dart';
import 'package:refena_flutter/refena_flutter.dart';
import 'package:rhttp/rhttp.dart';
import 'package:routerino/routerino.dart';
import 'package:uri_content/uri_content.dart';
import 'package:uuid/uuid.dart';
@@ -62,7 +62,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
required List<CrossFile> files,
required bool background,
}) async {
final requestDio = ref.read(dioProvider).longLiving;
final client = ref.read(httpProvider).longLiving;
final cancelToken = CancelToken();
final sessionId = _uuid.v4();
@@ -140,22 +140,23 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
);
}
Response? response;
HttpTextResponse? response;
bool invalidPin;
bool pinFirstAttempt = true;
String? pin;
do {
invalidPin = false;
try {
response = await requestDio.post(
ApiRoute.prepareUpload.target(target, query: {
response = await client.post(
ApiRoute.prepareUpload.target(target),
query: {
if (pin != null) 'pin': pin,
}),
data: jsonEncode(requestDto), // jsonEncode for better logging
},
body: HttpBody.json(requestDto.toJson()),
cancelToken: cancelToken,
);
} on DioException catch (e) {
switch (e.response?.statusCode) {
} on RhttpStatusCodeException catch (e) {
switch (e.statusCode) {
case 401:
invalidPin = true;
@@ -234,7 +235,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
final Map<String, String> fileMap;
if (target.version == '1.0') {
fileMap = (response.data as Map).cast<String, String>();
fileMap = (response.bodyToJson as Map).cast<String, String>();
} else {
if (response.statusCode == 204) {
// Nothing selected
@@ -242,7 +243,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
fileMap = {};
} else {
try {
final responseDto = PrepareUploadResponseDto.fromJson(response.data);
final responseDto = PrepareUploadResponseDto.fromJson(response.bodyToJson);
fileMap = responseDto.files;
state = state.updateSession(
sessionId: sessionId,
@@ -508,7 +509,7 @@ class SendNotifier extends Notifier<Map<String, SendSessionState>> {
// notify the receiver
try {
ref
.read(dioProvider)
.read(httpProvider)
.discovery
// ignore: discarded_futures
.post(ApiRoute.cancel.target(sessionState.target, query: remoteSessionId != null ? {'sessionId': remoteSessionId} : null));
@@ -607,17 +608,27 @@ extension on SendSessionState {
extension on Object {
String get humanErrorMessage {
final e = this;
if (e is DioException && e.response != null) {
final body = e.response!.data;
String message;
try {
message = (body as Map)['message'];
} catch (_) {
message = body;
}
return '[${e.response!.statusCode}] $message';
final (statusCode, message) = switch (this) {
RhttpStatusCodeException(:final statusCode, :final body) => (statusCode, _parseErrorMessage(body)),
_ => (null, e.toString()),
};
if (statusCode != null && message != null) {
return '[$statusCode] $message';
}
return e.toString();
}
}
String? _parseErrorMessage(Object? body) {
if (body is! String) {
return null;
}
try {
return (jsonDecode(body) as Map)['message'];
} catch (_) {
return null;
}
}
@@ -25,8 +25,8 @@ import 'package:localsend_app/pages/progress_page.dart';
import 'package:localsend_app/pages/receive_page.dart';
import 'package:localsend_app/pages/receive_page_controller.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/dio_provider.dart';
import 'package:localsend_app/provider/favorites_provider.dart';
import 'package:localsend_app/provider/http_provider.dart';
import 'package:localsend_app/provider/logging/discovery_logs_provider.dart';
import 'package:localsend_app/provider/network/nearby_devices_provider.dart';
import 'package:localsend_app/provider/network/send_provider.dart';
@@ -742,7 +742,7 @@ class ReceiveController {
// notify sender
try {
// ignore: unawaited_futures
server.ref.read(dioProvider).discovery.post(ApiRoute.cancel.target(session.sender, query: {'sessionId': session.sessionId}));
server.ref.read(httpProvider).discovery.post(ApiRoute.cancel.target(session.sender, query: {'sessionId': session.sessionId}));
} catch (e) {
_logger.warning('Failed to notify sender', e);
}
+19 -14
View File
@@ -8,20 +8,7 @@ class RhttpWrapper implements CustomHttpClient {
RhttpWrapper._(this._client);
factory RhttpWrapper.create(Duration timeout, StoredSecurityContext securityContext) {
final client = RhttpClient.createSync(
settings: ClientSettings(
timeoutSettings: TimeoutSettings(
timeout: timeout,
),
tlsSettings: TlsSettings(
verifyCertificates: false,
clientCertificate: ClientCertificate(
certificate: securityContext.certificate,
privateKey: securityContext.privateKey,
),
),
),
);
final client = createRhttpClient(timeout, securityContext);
return RhttpWrapper._(client);
}
@@ -79,3 +66,21 @@ class RhttpWrapper implements CustomHttpClient {
);
}
}
RhttpClient createRhttpClient(Duration timeout, StoredSecurityContext securityContext, {Interceptor? interceptor}) {
return RhttpClient.createSync(
settings: ClientSettings(
timeoutSettings: TimeoutSettings(
timeout: timeout,
),
tlsSettings: TlsSettings(
verifyCertificates: false,
clientCertificate: ClientCertificate(
certificate: securityContext.certificate,
privateKey: securityContext.privateKey,
),
),
),
interceptors: interceptor != null ? [interceptor] : [],
);
}
+2 -18
View File
@@ -349,22 +349,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
source: hosted
version: "5.7.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
dynamic_color:
dependency: "direct main"
description:
@@ -1319,10 +1303,10 @@ packages:
dependency: "direct main"
description:
name: rhttp
sha256: "8dc4e1b50cbbd4422a3da93accfd40ea96227fb7d2264d6856754122aedfb194"
sha256: "92fb57dea6338370efe1e4e2101e8b521f91f15bc60ef6908469b4392dd9803a"
url: "https://pub.dev"
source: hosted
version: "0.9.0"
version: "0.9.1"
routerino:
dependency: "direct main"
description:
+1 -2
View File
@@ -19,7 +19,6 @@ dependencies:
desktop_drop: 0.5.0
device_apps: 2.2.0
device_info_plus: 10.1.2
dio: 5.7.0
dynamic_color: 1.7.0
file_picker: 8.1.2
file_selector: 1.0.3
@@ -51,7 +50,7 @@ dependencies:
pretty_qr_code: 3.3.0
refena_flutter: 2.1.1
refena_inspector_client: 2.0.0
rhttp: 0.9.0
rhttp: 0.9.1
routerino: 0.8.0
saf_stream: 0.7.5
screen_retriever: 0.1.9
@@ -4,7 +4,6 @@ import 'package:common/model/device.dart';
import 'package:common/model/dto/info_dto.dart';
import 'package:common/src/isolate/child/http_provider.dart';
import 'package:common/src/isolate/child/sync_provider.dart';
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'package:refena/refena.dart';
@@ -37,9 +36,6 @@ class HttpTargetDiscoveryService {
});
final dto = InfoDtoMapper.deserialize(response);
return dto.toDevice(ip, port, https);
} on DioException catch (e) {
onError?.call(url, e);
return null;
} catch (e) {
onError?.call(url, e);
return null;
-37
View File
@@ -1,37 +0,0 @@
import 'dart:io';
import 'package:common/model/stored_security_context.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
/// It always trust the self signed certificate as all requests happen in a local network.
/// The user only needs to trust the local IP address.
/// Thanks to TCP (HTTP uses TCP), IP spoofing is nearly impossible.
Dio createDio(Duration timeout, StoredSecurityContext securityContext, {Interceptor? interceptor}) {
final dio = Dio(
BaseOptions(
connectTimeout: timeout,
sendTimeout: timeout,
),
);
// Allow any self signed certificate
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient(
context: SecurityContext()
..usePrivateKeyBytes(securityContext.privateKey.codeUnits)
..useCertificateChainBytes(securityContext.certificate.codeUnits),
);
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
);
// Add logging
if (interceptor != null) {
dio.interceptors.add(interceptor);
}
return dio;
}
-1
View File
@@ -9,7 +9,6 @@ environment:
dependencies:
collection: ^1.17.2 # allow newer versions, so it can compile with newer Flutter versions
dart_mappable: ^4.2.0
dio: 5.7.0
logging: ^1.2.0
meta: ^1.9.1
mime: ^1.0.4