diff --git a/android/build.gradle b/android/build.gradle index 49efb90..d3a03e9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'billion.group.wireguard_flutter' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.8.10' + ext.kotlin_version = '1.6.0' repositories { google() mavenCentral() diff --git a/android/src/main/kotlin/billion/group/wireguard_flutter/WireguardFlutterPlugin.kt b/android/src/main/kotlin/billion/group/wireguard_flutter/WireguardFlutterPlugin.kt index bae94d3..5f32100 100644 --- a/android/src/main/kotlin/billion/group/wireguard_flutter/WireguardFlutterPlugin.kt +++ b/android/src/main/kotlin/billion/group/wireguard_flutter/WireguardFlutterPlugin.kt @@ -134,12 +134,11 @@ class WireguardFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } } - override fun onMethodCall(call: MethodCall, result: Result) { - + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "initialize" -> setupTunnel(call.argument("localizedDescription").toString(), result) + "initialize" -> setupTunnel(call.argument("localizedDescription") ?: "", result) "start" -> { - connect(call.argument("wgQuickConfig").toString(), result) + connect(call.argument("wgQuickConfig") ?: "", result) if (!isVpnChecked) { if (isVpnActive()) { @@ -163,11 +162,19 @@ class WireguardFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, checkPermission() result.success(null) } + "getStats" -> { + if(tunnelName.isEmpty()){ + flutterError(result, "Invalid argument type for tunnel name") + }else{ + handleGetStats(result) + } + "getDownloadData" -> { getDownloadData(result) } "getUploadData" -> { getUploadData(result) + } else -> flutterNotImplemented(result) } @@ -290,6 +297,34 @@ class WireguardFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, } } + private fun handleGetStats(result: MethodChannel.Result) { + + if (tunnelName.isEmpty()) { + flutterError(result, "Provide tunnel name to get statistics") + return + } + + scope.launch(Dispatchers.IO) { + try { + val stats = futureBackend.await().getStatistics(tunnel(tunnelName)) + + var latestHandshake = 0L + + for (key in stats.peers()) { + val peerStats = stats.peer(key) + if (peerStats != null && peerStats.latestHandshakeEpochMillis > latestHandshake) { + latestHandshake = peerStats.latestHandshakeEpochMillis + } + } + flutterSuccess(result, Klaxon().toJsonString( + Stats(stats.totalRx(), stats.totalTx(), latestHandshake) + )) + } catch (e: BackendException) { + Log.e(TAG, "handleGetStats - BackendException - ERROR - ${e.reason}") + flutterError(result, e.reason.toString()) + } catch (e: Throwable) { + Log.e(TAG, "handleGetStats - Can't get stats: $e") + private fun getDownloadData(result: Result) { scope.launch(Dispatchers.IO) { try { @@ -341,3 +376,11 @@ class WireGuardTunnel( } } + + +class Stats( + val totalDownload: Long, + val totalUpload: Long, + val lastHandshake: Long +) + diff --git a/lib/linux/wireguard_flutter_linux.dart b/lib/linux/wireguard_flutter_linux.dart index 9350350..3825e81 100644 --- a/lib/linux/wireguard_flutter_linux.dart +++ b/lib/linux/wireguard_flutter_linux.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:process_run/shell.dart'; +import 'package:wireguard_flutter/model/stats.dart'; import '../wireguard_flutter_platform_interface.dart'; @@ -105,13 +106,43 @@ class WireGuardFlutterLinux extends WireGuardFlutterInterface { } @override - Future isConnected() async { + Future getStats() async { assert( - name != null, - 'Bad state: not initialized. Call "initialize" before calling this command', + (await isConnected()), + 'Bad state: vpn has not been started. Call startVpn', ); - final processResultList = await shell.run('sudo wg'); + + final processResultList = await shell.run('sudo wg show $name'); final process = processResultList.first; - return process.outLines.any((line) => line.trim() == 'interface: $name'); + final lines = process.outLines; + + if (lines.isEmpty) return null; + + num totalDownload = 0; + num totalUpload = 0; + int lastHandshake = 0; + + for (var line in lines) { + if (line.contains('transfer:')) { + var transferData = line.split(': ')[1].split(', '); + totalDownload += + int.tryParse(transferData[0].split(' ')[0].trim()) ?? 0; + totalUpload += int.tryParse(transferData[1].split(' ')[0].trim()) ?? 0; + } + if (line.contains('latest handshake:')) { + var handshakeData = line.split(': ')[1].trim(); + if (handshakeData != '0') { + // Parse the date and time + var dateTime = DateTime.parse(handshakeData); + lastHandshake = dateTime.millisecondsSinceEpoch; + } + } + } + + return Stats( + totalDownload: totalDownload, + totalUpload: totalUpload, + lastHandshake: lastHandshake, + ); } } diff --git a/lib/model/stats.dart b/lib/model/stats.dart new file mode 100644 index 0000000..479e8a0 --- /dev/null +++ b/lib/model/stats.dart @@ -0,0 +1,28 @@ +class Stats { + final num totalDownload; + final num totalUpload; + final num lastHandshake; + /// Constructor of the [Stats] class that receives [totalDownload] where total downloaded data is stored, + /// [totalUpload] where uploaded data is stored. + Stats({ + required this.totalDownload, + required this.totalUpload, + required this.lastHandshake + }); + + /// Method [toJson] to convert the class to JSON. + Map toJson() => { + 'totalDownload': totalDownload, + 'totalUpload': totalUpload, + 'lastHanshake' : lastHandshake + }; + + /// Method [Stats.fromJson] to convert the JSON to class. + factory Stats.fromJson(Map json) { + return Stats( + totalDownload: json['totalDownload'] as num, + totalUpload: json['totalUpload'] as num, + lastHandshake: json['lastHandshake'] as num + ); + } +} diff --git a/lib/wireguard_flutter.dart b/lib/wireguard_flutter.dart index 0fa8960..391808b 100644 --- a/lib/wireguard_flutter.dart +++ b/lib/wireguard_flutter.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:wireguard_flutter/linux/wireguard_flutter_linux.dart'; +import 'package:wireguard_flutter/model/stats.dart'; import 'package:wireguard_flutter/wireguard_flutter_method_channel.dart'; import 'wireguard_flutter_platform_interface.dart'; @@ -59,4 +60,7 @@ class WireGuardFlutter extends WireGuardFlutterInterface { @override Future stage() => _instance.stage(); + + @override + Future getStats() => _instance.getStats(); } diff --git a/lib/wireguard_flutter_method_channel.dart b/lib/wireguard_flutter_method_channel.dart index 6388f5e..170f112 100644 --- a/lib/wireguard_flutter_method_channel.dart +++ b/lib/wireguard_flutter_method_channel.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:flutter/services.dart'; +import 'package:wireguard_flutter/model/stats.dart'; import 'wireguard_flutter_platform_interface.dart'; @@ -57,4 +60,15 @@ class WireGuardFlutterMethodChannel extends WireGuardFlutterInterface { ) : VpnStage.disconnected, ); + + @override + Future getStats() async { + try { + final result = await _methodChannel.invokeMethod('getStats'); + final stats = Stats.fromJson(jsonDecode(result)); + return stats; + } on Exception catch (e) { + throw Exception(e); + } + } } diff --git a/lib/wireguard_flutter_platform_interface.dart b/lib/wireguard_flutter_platform_interface.dart index 3264554..b452bbe 100644 --- a/lib/wireguard_flutter_platform_interface.dart +++ b/lib/wireguard_flutter_platform_interface.dart @@ -1,3 +1,4 @@ +import 'model/stats.dart'; abstract class WireGuardFlutterInterface { Stream get vpnStageSnapshot; @@ -15,6 +16,7 @@ abstract class WireGuardFlutterInterface { Future stage(); Future isConnected() => stage().then((stage) => stage == VpnStage.connected); + Future getStats(); } enum VpnStage {