feat: add advanced setting to filter network interfaces (#2167)

This commit is contained in:
Tien Do Nam
2024-12-28 02:55:37 +01:00
committed by GitHub
parent aaeb54b4f5
commit 79e89c2ec2
28 changed files with 728 additions and 85 deletions
+1
View File
@@ -1,5 +1,6 @@
## 1.17.0 (unreleased)
- feat: add advanced setting to filter network interfaces (@Tienisto)
- feat(windows): when pasting an image, automatically convert it to PNG (@BrianMwit)
- feat(android): add option to open gallery when image/video was automatically saved (@Tienisto)
- fix: black screen when tapping on "Back" twice in "Share via link" (@Tienisto)
+12
View File
@@ -134,6 +134,11 @@
"deviceType": "Device type",
"deviceModel": "Device model",
"port": "Port",
"network": "Network",
"networkOptions": {
"all": "All",
"filtered": "Filtered"
},
"discoveryTimeout": "Discovery Timeout",
"useSystemName": "Use system name",
"generateRandomAlias": "Generate random alias",
@@ -170,6 +175,13 @@
"solution": "Does the problem exist on both sides? If so, you need to make sure that both devices are on the same Wi-Fi network and share the same configuration (port, multicast address, encryption). The Wi-Fi network may not allow communication between participants due to Access Point (AP) Isolation. In this case, this option must be disabled on the router."
}
},
"networkInterfacesPage": {
"title": "Network Interfaces",
"info": "By default, LocalSend uses all available network interfaces. You can exclude unwanted networks here. You need to restart the server to apply the changes.",
"preview": "Preview",
"whitelist": "Whitelist",
"blacklist": "Blacklist"
},
"receiveHistoryPage": {
"title": "History",
"openFolder": "Open folder",
+2
View File
@@ -156,6 +156,8 @@ Future<RefenaContainer> preInit(List<String> args) async {
deviceInfo: ref.read(deviceInfoProvider),
alias: settings.alias,
port: settings.port,
networkWhitelist: settings.networkWhitelist,
networkBlacklist: settings.networkBlacklist,
protocol: settings.https ? ProtocolType.https : ProtocolType.http,
multicastGroup: settings.multicastGroup,
discoveryTimeout: settings.discoveryTimeout,
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 52
/// Strings: 16374 (314 per locale)
/// Strings: 16382 (315 per locale)
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+29
View File
@@ -41,6 +41,7 @@ class Translations implements BaseTranslations<AppLocale, Translations> {
late final TranslationsSendTabEn sendTab = TranslationsSendTabEn.internal(_root);
late final TranslationsSettingsTabEn settingsTab = TranslationsSettingsTabEn.internal(_root);
late final TranslationsTroubleshootPageEn troubleshootPage = TranslationsTroubleshootPageEn.internal(_root);
late final TranslationsNetworkInterfacesPageEn networkInterfacesPage = TranslationsNetworkInterfacesPageEn.internal(_root);
late final TranslationsReceiveHistoryPageEn receiveHistoryPage = TranslationsReceiveHistoryPageEn.internal(_root);
late final TranslationsApkPickerPageEn apkPickerPage = TranslationsApkPickerPageEn.internal(_root);
late final TranslationsSelectedFilesPageEn selectedFilesPage = TranslationsSelectedFilesPageEn.internal(_root);
@@ -173,6 +174,21 @@ class TranslationsTroubleshootPageEn {
late final TranslationsTroubleshootPageNoConnectionEn noConnection = TranslationsTroubleshootPageNoConnectionEn.internal(_root);
}
// Path: networkInterfacesPage
class TranslationsNetworkInterfacesPageEn {
TranslationsNetworkInterfacesPageEn.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get title => 'Network Interfaces';
String get info =>
'By default, LocalSend uses all available network interfaces. You can exclude unwanted networks here. You need to restart the server to apply the changes.';
String get preview => 'Preview';
String get whitelist => 'Whitelist';
String get blacklist => 'Blacklist';
}
// Path: receiveHistoryPage
class TranslationsReceiveHistoryPageEn {
TranslationsReceiveHistoryPageEn.internal(this._root);
@@ -657,6 +673,8 @@ class TranslationsSettingsTabNetworkEn {
String get deviceType => 'Device type';
String get deviceModel => 'Device model';
String get port => 'Port';
String get network => 'Network';
late final TranslationsSettingsTabNetworkNetworkOptionsEn networkOptions = TranslationsSettingsTabNetworkNetworkOptionsEn.internal(_root);
String get discoveryTimeout => 'Discovery Timeout';
String get useSystemName => 'Use system name';
String get generateRandomAlias => 'Generate random alias';
@@ -1072,6 +1090,17 @@ class TranslationsSettingsTabGeneralLanguageOptionsEn {
String get system => 'System';
}
// Path: settingsTab.network.networkOptions
class TranslationsSettingsTabNetworkNetworkOptionsEn {
TranslationsSettingsTabNetworkNetworkOptionsEn.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
String get all => 'All';
String get filtered => 'Filtered';
}
// Path: progressPage.total.title
class TranslationsProgressPageTotalTitleEn {
TranslationsProgressPageTotalTitleEn.internal(this._root);
+4
View File
@@ -15,6 +15,8 @@ class SettingsState with SettingsStateMappable {
final ColorMode colorMode;
final AppLocale? locale;
final int port;
final List<String>? networkWhitelist; // null = disabled
final List<String>? networkBlacklist; // null = disabled
final String multicastGroup;
final String? destination; // null = default
final bool saveToGallery; // only Android, iOS
@@ -41,6 +43,8 @@ class SettingsState with SettingsStateMappable {
required this.colorMode,
required this.locale,
required this.port,
required this.networkWhitelist,
required this.networkBlacklist,
required this.multicastGroup,
required this.destination,
required this.saveToGallery,
@@ -33,6 +33,10 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
static const Field<SettingsState, AppLocale> _f$locale = Field('locale', _$locale);
static int _$port(SettingsState v) => v.port;
static const Field<SettingsState, int> _f$port = Field('port', _$port);
static List<String>? _$networkWhitelist(SettingsState v) => v.networkWhitelist;
static const Field<SettingsState, List<String>> _f$networkWhitelist = Field('networkWhitelist', _$networkWhitelist);
static List<String>? _$networkBlacklist(SettingsState v) => v.networkBlacklist;
static const Field<SettingsState, List<String>> _f$networkBlacklist = Field('networkBlacklist', _$networkBlacklist);
static String _$multicastGroup(SettingsState v) => v.multicastGroup;
static const Field<SettingsState, String> _f$multicastGroup = Field('multicastGroup', _$multicastGroup);
static String? _$destination(SettingsState v) => v.destination;
@@ -78,6 +82,8 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
#colorMode: _f$colorMode,
#locale: _f$locale,
#port: _f$port,
#networkWhitelist: _f$networkWhitelist,
#networkBlacklist: _f$networkBlacklist,
#multicastGroup: _f$multicastGroup,
#destination: _f$destination,
#saveToGallery: _f$saveToGallery,
@@ -106,6 +112,8 @@ class SettingsStateMapper extends ClassMapperBase<SettingsState> {
colorMode: data.dec(_f$colorMode),
locale: data.dec(_f$locale),
port: data.dec(_f$port),
networkWhitelist: data.dec(_f$networkWhitelist),
networkBlacklist: data.dec(_f$networkBlacklist),
multicastGroup: data.dec(_f$multicastGroup),
destination: data.dec(_f$destination),
saveToGallery: data.dec(_f$saveToGallery),
@@ -170,6 +178,8 @@ extension SettingsStateValueCopy<$R, $Out> on ObjectCopyWith<$R, SettingsState,
}
abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out> implements ClassCopyWith<$R, $In, $Out> {
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkWhitelist;
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkBlacklist;
$R call(
{String? showToken,
String? alias,
@@ -177,6 +187,8 @@ abstract class SettingsStateCopyWith<$R, $In extends SettingsState, $Out> implem
ColorMode? colorMode,
AppLocale? locale,
int? port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
String? multicastGroup,
String? destination,
bool? saveToGallery,
@@ -205,6 +217,14 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting
@override
late final ClassMapperBase<SettingsState> $mapper = SettingsStateMapper.ensureInitialized();
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkWhitelist => $value.networkWhitelist != null
? ListCopyWith($value.networkWhitelist!, (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(networkWhitelist: v))
: null;
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkBlacklist => $value.networkBlacklist != null
? ListCopyWith($value.networkBlacklist!, (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(networkBlacklist: v))
: null;
@override
$R call(
{String? showToken,
String? alias,
@@ -212,6 +232,8 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting
ColorMode? colorMode,
Object? locale = $none,
int? port,
Object? networkWhitelist = $none,
Object? networkBlacklist = $none,
String? multicastGroup,
Object? destination = $none,
bool? saveToGallery,
@@ -237,6 +259,8 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting
if (colorMode != null) #colorMode: colorMode,
if (locale != $none) #locale: locale,
if (port != null) #port: port,
if (networkWhitelist != $none) #networkWhitelist: networkWhitelist,
if (networkBlacklist != $none) #networkBlacklist: networkBlacklist,
if (multicastGroup != null) #multicastGroup: multicastGroup,
if (destination != $none) #destination: destination,
if (saveToGallery != null) #saveToGallery: saveToGallery,
@@ -264,6 +288,8 @@ class _SettingsStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, Setting
colorMode: data.get(#colorMode, or: $value.colorMode),
locale: data.get(#locale, or: $value.locale),
port: data.get(#port, or: $value.port),
networkWhitelist: data.get(#networkWhitelist, or: $value.networkWhitelist),
networkBlacklist: data.get(#networkBlacklist, or: $value.networkBlacklist),
multicastGroup: data.get(#multicastGroup, or: $value.multicastGroup),
destination: data.get(#destination, or: $value.destination),
saveToGallery: data.get(#saveToGallery, or: $value.saveToGallery),
@@ -0,0 +1,223 @@
import 'package:collection/collection.dart';
import 'package:common/util/network_interfaces.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:local_hero/local_hero.dart';
import 'package:localsend_app/gen/strings.g.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/widget/dialogs/text_field_tv.dart';
import 'package:localsend_app/widget/labeled_checkbox.dart';
import 'package:localsend_app/widget/responsive_list_view.dart';
import 'package:moform/moform.dart';
import 'package:refena_flutter/refena_flutter.dart';
class NetworkInterfacesPage extends StatefulWidget {
const NetworkInterfacesPage();
@override
State<NetworkInterfacesPage> createState() => _NetworkInterfacesPageState();
}
class _NetworkInterfacesPageState extends State<NetworkInterfacesPage> {
List<(String, List<String>)> rawInterfaces = [];
@override
void initState() {
super.initState();
// ignore: discarded_futures
getNetworkInterfaces(whitelist: null, blacklist: null).then((value) {
if (mounted) {
setState(() {
rawInterfaces = value.map((e) => (e.name, e.addresses.map((a) => a.address).toList())).toList();
});
}
});
}
@override
Widget build(BuildContext context) {
final settings = context.watch(settingsProvider);
final currList = settings.networkWhitelist ?? settings.networkBlacklist ?? [];
final Future<void> Function(List<String>?) updateFunction = settings.networkWhitelist != null
? context.notifier(settingsProvider).setNetworkWhitelist
: context.notifier(settingsProvider).setNetworkBlacklist;
return Scaffold(
appBar: AppBar(
title: Text(t.networkInterfacesPage.title),
),
body: LocalHeroScope(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: ResponsiveListView(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20),
children: [
Text(
t.networkInterfacesPage.info,
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
t.networkInterfacesPage.preview,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
ScrollConfiguration(
// By default, Flutter only allows dragging with touch devices.
// We also allow dragging with mouse.
behavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.trackpad,
PointerDeviceKind.unknown,
},
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: rawInterfaces.mapIndexed((i, e) {
final ignored = isNetworkIgnoredRaw(
networkWhitelist: settings.networkWhitelist,
networkBlacklist: settings.networkBlacklist,
interface: e.$2,
);
final style = ignored
? const TextStyle(
color: Colors.grey,
decoration: TextDecoration.lineThrough,
)
: null;
return Padding(
padding: const EdgeInsets.only(right: 10),
child: Card(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('[#${i + 1}] ${e.$1}', style: style),
...e.$2.map((ip) => Text(ip, style: style)),
],
),
),
),
);
}).toList(),
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
LabeledCheckbox(
label: t.networkInterfacesPage.whitelist,
value: settings.networkWhitelist != null,
onChanged: (value) async {
if (value == false) {
await context.notifier(settingsProvider).setNetworkWhitelist(null);
} else {
await context.notifier(settingsProvider).setNetworkWhitelist(switch (currList) {
[] => [''],
_ => [...currList],
});
if (context.mounted) {
await context.notifier(settingsProvider).setNetworkBlacklist(null);
}
}
},
),
LabeledCheckbox(
label: t.networkInterfacesPage.blacklist,
value: settings.networkBlacklist != null,
onChanged: (value) async {
if (value == false) {
await context.notifier(settingsProvider).setNetworkBlacklist(null);
} else {
await context.notifier(settingsProvider).setNetworkBlacklist(switch (currList) {
[] => [''],
_ => [...currList],
});
if (context.mounted) {
await context.notifier(settingsProvider).setNetworkWhitelist(null);
}
}
},
),
],
),
const SizedBox(height: 20),
...currList.mapIndexed((i, e) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: StringField(
value: e,
onChanged: (value) async {
await updateFunction([
...currList.sublist(0, i),
value,
...currList.sublist(i + 1),
]);
},
builder: (context, controller) {
return TextFieldTv(
name: t.networkInterfacesPage.whitelist,
controller: controller,
onDelete: () async {
if (currList.length == 1) {
await updateFunction(null);
return;
}
await updateFunction([
...currList.sublist(0, i),
...currList.sublist(i + 1),
]);
},
);
}),
);
}),
if (settings.networkWhitelist != null || settings.networkBlacklist != null)
LocalHero(
tag: 'network_interfaces_bottom',
child: Row(
children: [
Material(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${t.general.example}:',
),
Text('123.123.123.123'),
Text('123.123.123.*'),
],
),
),
const Spacer(),
FilledButton.icon(
onPressed: () async {
await updateFunction([
...currList,
'',
]);
},
icon: const Icon(Icons.add),
label: Text(t.general.add),
),
],
),
),
],
),
),
);
}
}
+12
View File
@@ -10,6 +10,7 @@ import 'package:localsend_app/pages/about/about_page.dart';
import 'package:localsend_app/pages/changelog_page.dart';
import 'package:localsend_app/pages/donation/donation_page.dart';
import 'package:localsend_app/pages/language_page.dart';
import 'package:localsend_app/pages/settings/network_interfaces_page.dart';
import 'package:localsend_app/pages/tabs/settings_tab_controller.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/provider/version_provider.dart';
@@ -401,6 +402,17 @@ class SettingsTab extends StatelessWidget {
},
),
),
if (vm.advanced)
_ButtonEntry(
label: t.settingsTab.network.network,
buttonLabel: switch (vm.settings.networkWhitelist != null || vm.settings.networkBlacklist != null) {
true => t.settingsTab.network.networkOptions.filtered,
false => t.settingsTab.network.networkOptions.all,
},
onTap: () async {
await context.push(() => const NetworkInterfacesPage());
},
),
if (vm.advanced)
_SettingsEntry(
label: t.settingsTab.network.discoveryTimeout,
@@ -1,3 +1,4 @@
import 'package:common/isolate.dart';
import 'package:common/model/device_info_result.dart';
import 'package:common/util/sleep.dart';
import 'package:flutter/material.dart';
@@ -6,6 +7,7 @@ import 'package:localsend_app/model/persistence/color_mode.dart';
import 'package:localsend_app/pages/language_page.dart';
import 'package:localsend_app/pages/tabs/settings_tab_vm.dart';
import 'package:localsend_app/provider/device_info_provider.dart';
import 'package:localsend_app/provider/local_ip_provider.dart';
import 'package:localsend_app/provider/network/server/server_provider.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/native/autostart_helper.dart';
@@ -18,12 +20,16 @@ import 'package:routerino/routerino.dart';
final settingsTabControllerProvider = ReduxProvider<SettingsTabController, SettingsTabVm>((ref) {
final settings = ref.notifier(settingsProvider);
final server = ref.notifier(serverProvider);
final isolateController = ref.notifier(parentIsolateProvider);
final localIpService = ref.notifier(localIpProvider);
final initialDeviceInfo = ref.read(deviceInfoProvider);
final supportsDynamicColors = ref.read(dynamicColorsProvider) != null;
return SettingsTabController(
settingsService: settings,
serverNotifier: server,
isolateController: isolateController,
localIpService: localIpService,
initialDeviceInfo: initialDeviceInfo,
supportsDynamicColors: supportsDynamicColors,
);
@@ -32,16 +38,22 @@ final settingsTabControllerProvider = ReduxProvider<SettingsTabController, Setti
class SettingsTabController extends ReduxNotifier<SettingsTabVm> {
final SettingsService _settingsService;
final ServerService _serverService;
final IsolateController _isolateController;
final LocalIpService _localIpService;
final DeviceInfoResult _initialDeviceInfo;
final bool _supportsDynamicColors;
SettingsTabController({
required SettingsService settingsService,
required ServerService serverNotifier,
required IsolateController isolateController,
required LocalIpService localIpService,
required DeviceInfoResult initialDeviceInfo,
required bool supportsDynamicColors,
}) : _settingsService = settingsService,
_serverService = serverNotifier,
_isolateController = isolateController,
_localIpService = localIpService,
_initialDeviceInfo = initialDeviceInfo,
_supportsDynamicColors = supportsDynamicColors;
@@ -123,6 +135,8 @@ class SettingsTabController extends ReduxNotifier<SettingsTabVm> {
state.portController.text = newServerState.port.toString();
await _settingsService.setAlias(newServerState.alias);
await _settingsService.setPort(newServerState.port);
external(_isolateController).dispatch(IsolateSendMulticastRestartListenerAction());
external(_localIpService).dispatchAsync(FetchLocalIpAction()); // ignore: unawaited_futures
}
} catch (e) {
// ignore: use_build_context_synchronously
+28 -19
View File
@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:common/util/network_interfaces.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:localsend_app/model/state/network_state.dart';
import 'package:localsend_app/provider/settings_provider.dart';
import 'package:localsend_app/util/native/platform_check.dart';
import 'package:logging/logging.dart';
import 'package:network_info_plus/network_info_plus.dart' as plugin;
@@ -13,13 +14,17 @@ import 'package:refena_flutter/refena_flutter.dart';
final _logger = Logger('NetworkInfo');
final localIpProvider = ReduxProvider<LocalIpService, NetworkState>((ref) {
return LocalIpService();
return LocalIpService(
ref.notifier(settingsProvider),
);
});
StreamSubscription? _subscription;
class LocalIpService extends ReduxNotifier<NetworkState> {
LocalIpService();
final SettingsService _settingsService;
LocalIpService(this._settingsService);
@override
NetworkState init() {
@@ -44,11 +49,11 @@ class InitLocalIpAction extends ReduxAction<LocalIpService, NetworkState> {
if (checkPlatform([TargetPlatform.windows])) {
// https://github.com/localsend/localsend/issues/12
_subscription = Stream.periodic(const Duration(seconds: 5), (_) {}).listen((_) async {
await dispatchAsync(_FetchLocalIpAction());
await dispatchAsync(FetchLocalIpAction());
});
} else {
_subscription = Connectivity().onConnectivityChanged.listen((_) async {
await dispatchAsync(_FetchLocalIpAction());
await dispatchAsync(FetchLocalIpAction());
});
}
}
@@ -59,21 +64,27 @@ class InitLocalIpAction extends ReduxAction<LocalIpService, NetworkState> {
@override
void after() {
// ignore: discarded_futures
dispatchAsync(_FetchLocalIpAction());
dispatchAsync(FetchLocalIpAction());
}
}
class _FetchLocalIpAction extends AsyncReduxAction<LocalIpService, NetworkState> {
class FetchLocalIpAction extends AsyncReduxAction<LocalIpService, NetworkState> {
@override
Future<NetworkState> reduce() async {
return NetworkState(
localIps: await _getIp(),
localIps: await _getIp(
whitelist: notifier._settingsService.state.networkWhitelist,
blacklist: notifier._settingsService.state.networkBlacklist,
),
initialized: true,
);
}
}
Future<List<String>> _getIp() async {
Future<List<String>> _getIp({
required List<String>? whitelist,
required List<String>? blacklist,
}) async {
final info = plugin.NetworkInfo();
String? ip;
try {
@@ -82,16 +93,14 @@ Future<List<String>> _getIp() async {
_logger.warning('Failed to get wifi IP', e);
}
List<String> nativeResult = [];
if (!kIsWeb) {
try {
// fallback with dart:io NetworkInterface
final result = (await NetworkInterface.list()).map((networkInterface) => networkInterface.addresses).expand((ip) => ip);
nativeResult = result.where((ip) => ip.type == InternetAddressType.IPv4).map((address) => address.address).toList();
} catch (e, st) {
_logger.info('Failed to get IP from dart:io', e, st);
}
}
final nativeResult = (await getNetworkInterfaces(
whitelist: whitelist,
blacklist: blacklist,
))
.map((interface) => interface.addresses.map((a) => a.address).toList())
.expand((ip) => ip)
.where((ip) => !ip.contains(':')) // ignore IPv6 for now
.toList();
final addresses = rankIpAddresses(nativeResult, ip);
_logger.info('Network state: $addresses');
@@ -66,6 +66,8 @@ const _themeKey = 'ls_theme'; // now called brightness
const _colorKey = 'ls_color';
const _localeKey = 'ls_locale';
const _portKey = 'ls_port';
const _networkWhitelistKey = 'ls_network_whitelist';
const _networkBlacklistKey = 'ls_network_blacklist';
const _timeoutKey = 'ls_timeout';
const _multicastGroupKey = 'ls_multicast_group';
const _destinationKey = 'ls_destination';
@@ -286,6 +288,30 @@ class PersistenceService {
await _prefs.setInt(_portKey, port);
}
List<String>? getNetworkWhitelist() {
return _prefs.getStringList(_networkWhitelistKey);
}
Future<void> setNetworkWhitelist(List<String>? whitelist) async {
if (whitelist == null) {
await _prefs.remove(_networkWhitelistKey);
} else {
await _prefs.setStringList(_networkWhitelistKey, whitelist);
}
}
List<String>? getNetworkBlacklist() {
return _prefs.getStringList(_networkBlacklistKey);
}
Future<void> setNetworkBlacklist(List<String>? blacklist) async {
if (blacklist == null) {
await _prefs.remove(_networkBlacklistKey);
} else {
await _prefs.setStringList(_networkBlacklistKey, blacklist);
}
}
int getDiscoveryTimeout() {
return _prefs.getInt(_timeoutKey) ?? defaultDiscoveryTimeout;
}
+25 -1
View File
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:common/isolate.dart';
import 'package:common/model/device.dart';
import 'package:flutter/material.dart';
@@ -8,15 +9,22 @@ import 'package:localsend_app/model/state/settings_state.dart';
import 'package:localsend_app/provider/persistence_provider.dart';
import 'package:refena_flutter/refena_flutter.dart';
final _listEq = const ListEquality().equals;
final settingsProvider = NotifierProvider<SettingsService, SettingsState>((ref) {
return SettingsService(ref.read(persistenceProvider));
}, onChanged: (_, next, ref) {
final syncState = ref.read(parentIsolateProvider).syncState;
if (syncState.multicastGroup == next.multicastGroup && syncState.discoveryTimeout == next.discoveryTimeout) {
if (_listEq(syncState.networkWhitelist, next.networkWhitelist) &&
_listEq(syncState.networkBlacklist, next.networkBlacklist) &&
syncState.multicastGroup == next.multicastGroup &&
syncState.discoveryTimeout == next.discoveryTimeout) {
return;
}
ref.redux(parentIsolateProvider).dispatch(IsolateSyncSettingsAction(
networkWhitelist: next.networkWhitelist,
networkBlacklist: next.networkBlacklist,
multicastGroup: next.multicastGroup,
discoveryTimeout: next.discoveryTimeout,
));
@@ -35,6 +43,8 @@ class SettingsService extends PureNotifier<SettingsState> {
colorMode: _persistence.getColorMode(),
locale: _persistence.getLocale(),
port: _persistence.getPort(),
networkWhitelist: _persistence.getNetworkWhitelist(),
networkBlacklist: _persistence.getNetworkBlacklist(),
multicastGroup: _persistence.getMulticastGroup(),
destination: _persistence.getDestination(),
saveToGallery: _persistence.isSaveToGallery(),
@@ -97,6 +107,20 @@ class SettingsService extends PureNotifier<SettingsState> {
);
}
Future<void> setNetworkWhitelist(List<String>? whitelist) async {
await _persistence.setNetworkWhitelist(whitelist);
state = state.copyWith(
networkWhitelist: whitelist,
);
}
Future<void> setNetworkBlacklist(List<String>? blacklist) async {
await _persistence.setNetworkBlacklist(blacklist);
state = state.copyWith(
networkBlacklist: blacklist,
);
}
Future<void> setDiscoveryTimeout(int timeout) async {
await _persistence.setDiscoveryTimeout(timeout);
state = state.copyWith(
+14 -2
View File
@@ -10,12 +10,14 @@ import 'package:routerino/routerino.dart';
class TextFieldTv extends StatefulWidget {
final String name;
final TextEditingController controller;
final ValueChanged<String> onChanged;
final ValueChanged<String>? onChanged;
final VoidCallback? onDelete;
const TextFieldTv({
required this.name,
required this.controller,
required this.onChanged,
this.onChanged,
this.onDelete,
});
@override
@@ -71,6 +73,16 @@ class _TextFieldTvState extends State<TextFieldTv> with Refena {
controller: widget.controller,
textAlign: TextAlign.center,
onChanged: widget.onChanged,
decoration: InputDecoration(
suffixIcon: widget.onDelete != null
? IconButton(
icon: Icon(Icons.clear),
onPressed: () {
widget.onDelete?.call();
},
)
: null,
),
);
}
}
+16
View File
@@ -857,6 +857,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
local_hero:
dependency: "direct main"
description:
name: local_hero
sha256: "5c85451dd51ecd0e8d3656775fac9a6db82f296f200d9931217186d34fed6089"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
logging:
dependency: "direct main"
description:
@@ -937,6 +945,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.4.4"
moform:
dependency: "direct main"
description:
name: moform
sha256: "4ef955b2422b0c7c676128b398e417b191647b6a1259d3928000a9a40c63f571"
url: "https://pub.dev"
source: hosted
version: "0.2.5"
nanoid2:
dependency: "direct main"
description:
+4 -2
View File
@@ -29,15 +29,17 @@ dependencies:
sdk: flutter
flutter_markdown: 0.7.4+2
gal: 2.3.0
image: ^4.3.0
image: 4.3.0
image_picker: 1.1.2
in_app_purchase: 3.2.0 # [FOSS_REMOVE]
intl: ^0.19.0 # allow newer versions, so it can compile with newer Flutter versions
legalize: ^1.2.2
legalize: 1.2.2
local_hero: 0.3.0
logging: 1.3.0
# https://github.com/NightFeather0615/macos_dock_progress/issues/1
# macos_dock_progress: 1.1.0
mime: 1.0.6
moform: 0.2.5
nanoid2: 2.0.1
network_info_plus: 6.1.1
open_dir: 0.0.2+1
+20
View File
@@ -258,6 +258,26 @@ class MockPersistenceService extends _i1.Mock implements _i3.PersistenceService
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> setNetworkWhitelist(List<String>? whitelist) => (super.noSuchMethod(
Invocation.method(
#setNetworkWhitelist,
[whitelist],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
_i4.Future<void> setNetworkBlacklist(List<String>? blacklist) => (super.noSuchMethod(
Invocation.method(
#setNetworkBlacklist,
[blacklist],
),
returnValue: _i4.Future<void>.value(),
returnValueForMissingStub: _i4.Future<void>.value(),
) as _i4.Future<void>);
@override
int getDiscoveryTimeout() => (super.noSuchMethod(
Invocation.method(
+1 -17
View File
@@ -93,22 +93,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.2.2"
dio:
dependency: transitive
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"
file:
dependency: transitive
description:
@@ -446,4 +430,4 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
dart: ">=3.5.0 <4.0.0"
@@ -4,21 +4,30 @@ import 'package:common/src/isolate/dto/send_to_isolate_data.dart';
import 'package:common/src/task/discovery/multicast_discovery.dart';
import 'package:meta/meta.dart';
sealed class MulticastTask {}
/// Sends an announcement to all devices to all network interfaces.
/// They will respond with their device information.
///
/// This is not wrapped in an [IsolateTask] because
/// - (1) it is not important if the device was found by this specific announcement or another one
/// - (2) it is not 100% accurate to know if a device was found by this announcement or another one
class MulticastAnnouncementTask {
class MulticastAnnouncementTask implements MulticastTask {
static const instance = MulticastAnnouncementTask._();
const MulticastAnnouncementTask._();
}
/// Restarts the listener.
class MulticastRestartListenerTask implements MulticastTask {
static const instance = MulticastRestartListenerTask._();
const MulticastRestartListenerTask._();
}
@internal
Future<void> setupMulticastDiscoveryIsolate(
Stream<SendToIsolateData<MulticastAnnouncementTask>> receiveFromMain,
Stream<SendToIsolateData<MulticastTask>> receiveFromMain,
void Function(Device) sendToMain,
InitialData initialData,
) async {
@@ -33,7 +42,14 @@ Future<void> setupMulticastDiscoveryIsolate(
});
},
handler: (ref, task) async {
switch (task) {
case MulticastAnnouncementTask():
await ref.read(multicastDiscoveryProvider).sendAnnouncement();
break;
case MulticastRestartListenerTask():
ref.read(multicastDiscoveryProvider).restartListener();
break;
}
},
);
}
@@ -19,6 +19,8 @@ class SyncState with SyncStateMappable {
final DeviceInfoResult deviceInfo;
final String alias;
final int port;
final List<String>? networkWhitelist;
final List<String>? networkBlacklist;
final ProtocolType protocol;
final String multicastGroup;
final int discoveryTimeout;
@@ -34,6 +36,8 @@ class SyncState with SyncStateMappable {
required this.deviceInfo,
required this.alias,
required this.port,
required this.networkWhitelist,
required this.networkBlacklist,
required this.protocol,
required this.multicastGroup,
required this.discoveryTimeout,
@@ -43,7 +47,7 @@ class SyncState with SyncStateMappable {
@override
String toString() {
return 'SyncState(securityContext: <SecurityContext>, deviceInfo: $deviceInfo, alias: $alias, port: $port, protocol: $protocol, multicastGroup: $multicastGroup, discoveryTimeout: $discoveryTimeout, serverRunning: $serverRunning, download: $download)';
return 'SyncState(securityContext: <SecurityContext>, deviceInfo: $deviceInfo, alias: $alias, port: $port, networkWhitelist: $networkWhitelist, networkBlacklist: $networkBlacklist, protocol: $protocol, multicastGroup: $multicastGroup, discoveryTimeout: $discoveryTimeout, serverRunning: $serverRunning, download: $download)';
}
}
@@ -38,6 +38,10 @@ class SyncStateMapper extends ClassMapperBase<SyncState> {
static const Field<SyncState, String> _f$alias = Field('alias', _$alias);
static int _$port(SyncState v) => v.port;
static const Field<SyncState, int> _f$port = Field('port', _$port);
static List<String>? _$networkWhitelist(SyncState v) => v.networkWhitelist;
static const Field<SyncState, List<String>> _f$networkWhitelist = Field('networkWhitelist', _$networkWhitelist);
static List<String>? _$networkBlacklist(SyncState v) => v.networkBlacklist;
static const Field<SyncState, List<String>> _f$networkBlacklist = Field('networkBlacklist', _$networkBlacklist);
static ProtocolType _$protocol(SyncState v) => v.protocol;
static const Field<SyncState, ProtocolType> _f$protocol = Field('protocol', _$protocol);
static String _$multicastGroup(SyncState v) => v.multicastGroup;
@@ -58,6 +62,8 @@ class SyncStateMapper extends ClassMapperBase<SyncState> {
#deviceInfo: _f$deviceInfo,
#alias: _f$alias,
#port: _f$port,
#networkWhitelist: _f$networkWhitelist,
#networkBlacklist: _f$networkBlacklist,
#protocol: _f$protocol,
#multicastGroup: _f$multicastGroup,
#discoveryTimeout: _f$discoveryTimeout,
@@ -74,6 +80,8 @@ class SyncStateMapper extends ClassMapperBase<SyncState> {
deviceInfo: data.dec(_f$deviceInfo),
alias: data.dec(_f$alias),
port: data.dec(_f$port),
networkWhitelist: data.dec(_f$networkWhitelist),
networkBlacklist: data.dec(_f$networkBlacklist),
protocol: data.dec(_f$protocol),
multicastGroup: data.dec(_f$multicastGroup),
discoveryTimeout: data.dec(_f$discoveryTimeout),
@@ -125,6 +133,8 @@ extension SyncStateValueCopy<$R, $Out> on ObjectCopyWith<$R, SyncState, $Out> {
abstract class SyncStateCopyWith<$R, $In extends SyncState, $Out> implements ClassCopyWith<$R, $In, $Out> {
StoredSecurityContextCopyWith<$R, StoredSecurityContext, StoredSecurityContext> get securityContext;
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkWhitelist;
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkBlacklist;
$R call(
{Future<void> Function()? init,
Object? rootIsolateToken,
@@ -133,6 +143,8 @@ abstract class SyncStateCopyWith<$R, $In extends SyncState, $Out> implements Cla
DeviceInfoResult? deviceInfo,
String? alias,
int? port,
List<String>? networkWhitelist,
List<String>? networkBlacklist,
ProtocolType? protocol,
String? multicastGroup,
int? discoveryTimeout,
@@ -150,6 +162,14 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
StoredSecurityContextCopyWith<$R, StoredSecurityContext, StoredSecurityContext> get securityContext =>
$value.securityContext.copyWith.$chain((v) => call(securityContext: v));
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkWhitelist => $value.networkWhitelist != null
? ListCopyWith($value.networkWhitelist!, (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(networkWhitelist: v))
: null;
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>>? get networkBlacklist => $value.networkBlacklist != null
? ListCopyWith($value.networkBlacklist!, (v, t) => ObjectCopyWith(v, $identity, t), (v) => call(networkBlacklist: v))
: null;
@override
$R call(
{Future<void> Function()? init,
Object? rootIsolateToken,
@@ -158,6 +178,8 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
DeviceInfoResult? deviceInfo,
String? alias,
int? port,
Object? networkWhitelist = $none,
Object? networkBlacklist = $none,
ProtocolType? protocol,
String? multicastGroup,
int? discoveryTimeout,
@@ -171,6 +193,8 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
if (deviceInfo != null) #deviceInfo: deviceInfo,
if (alias != null) #alias: alias,
if (port != null) #port: port,
if (networkWhitelist != $none) #networkWhitelist: networkWhitelist,
if (networkBlacklist != $none) #networkBlacklist: networkBlacklist,
if (protocol != null) #protocol: protocol,
if (multicastGroup != null) #multicastGroup: multicastGroup,
if (discoveryTimeout != null) #discoveryTimeout: discoveryTimeout,
@@ -186,6 +210,8 @@ class _SyncStateCopyWithImpl<$R, $Out> extends ClassCopyWithBase<$R, SyncState,
deviceInfo: data.get(#deviceInfo, or: $value.deviceInfo),
alias: data.get(#alias, or: $value.alias),
port: data.get(#port, or: $value.port),
networkWhitelist: data.get(#networkWhitelist, or: $value.networkWhitelist),
networkBlacklist: data.get(#networkBlacklist, or: $value.networkBlacklist),
protocol: data.get(#protocol, or: $value.protocol),
multicastGroup: data.get(#multicastGroup, or: $value.multicastGroup),
discoveryTimeout: data.get(#discoveryTimeout, or: $value.discoveryTimeout),
+17 -2
View File
@@ -131,8 +131,6 @@ class IsolateFavoriteHttpDiscoveryAction extends ReduxActionWithResult<IsolateCo
}
class IsolateSendMulticastAnnouncementAction extends ReduxAction<IsolateController, ParentIsolateState> {
IsolateSendMulticastAnnouncementAction();
@override
ParentIsolateState reduce() {
final connection = state.multicastDiscovery;
@@ -149,6 +147,23 @@ class IsolateSendMulticastAnnouncementAction extends ReduxAction<IsolateControll
}
}
class IsolateSendMulticastRestartListenerAction extends ReduxAction<IsolateController, ParentIsolateState> {
@override
ParentIsolateState reduce() {
final connection = state.multicastDiscovery;
if (connection == null) {
throw StateError('multicastDiscovery is not initialized');
}
connection.sendToIsolate(SendToIsolateData(
syncState: null,
data: MulticastRestartListenerTask.instance,
));
return state;
}
}
class IsolateHttpUploadActionResult {
final int taskId;
final Stream<double> progress;
@@ -45,10 +45,14 @@ class IsolateSyncDeviceInfoAction extends ReduxAction<IsolateController, ParentI
}
class IsolateSyncSettingsAction extends ReduxAction<IsolateController, ParentIsolateState> {
final List<String>? networkWhitelist;
final List<String>? networkBlacklist;
final String multicastGroup;
final int discoveryTimeout;
IsolateSyncSettingsAction({
required this.networkWhitelist,
required this.networkBlacklist,
required this.multicastGroup,
required this.discoveryTimeout,
});
@@ -57,6 +61,8 @@ class IsolateSyncSettingsAction extends ReduxAction<IsolateController, ParentIso
ParentIsolateState reduce() {
dispatch(_PublishSyncStateAction(
syncState: state.syncState.copyWith(
networkWhitelist: networkWhitelist,
networkBlacklist: networkBlacklist,
multicastGroup: multicastGroup,
discoveryTimeout: discoveryTimeout,
),
@@ -25,7 +25,7 @@ class ParentIsolateState with ParentIsolateStateMappable {
final SyncState syncState;
final IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<HttpScanTask>>>? httpScanDiscovery;
final IsolateConnector<IsolateTaskResult<Device>, SendToIsolateData<IsolateTask<HttpTargetTask>>>? httpTargetDiscovery;
final IsolateConnector<Device, SendToIsolateData<MulticastAnnouncementTask>>? multicastDiscovery;
final IsolateConnector<Device, SendToIsolateData<MulticastTask>>? multicastDiscovery;
final List<IsolateConnector<IsolateTaskStreamResult<double>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>> httpUpload;
int get uploadIsolateCount => httpUpload.length;
@@ -94,7 +94,7 @@ class IsolateSetupAction extends AsyncReduxAction<IsolateController, ParentIsola
),
);
final multicastDiscovery = await startIsolate<Device, SendToIsolateData<MulticastAnnouncementTask>, InitialData>(
final multicastDiscovery = await startIsolate<Device, SendToIsolateData<MulticastTask>, InitialData>(
task: setupMulticastDiscoveryIsolate,
param: InitialData(
syncState: state.syncState,
@@ -32,8 +32,8 @@ class ParentIsolateStateMapper extends ClassMapperBase<ParentIsolateState> {
v.httpTargetDiscovery;
static const Field<ParentIsolateState, IsolateConnector<IsolateTaskResult<Device>, SendToIsolateData<IsolateTask<HttpTargetTask>>>>
_f$httpTargetDiscovery = Field('httpTargetDiscovery', _$httpTargetDiscovery);
static IsolateConnector<Device, SendToIsolateData<MulticastAnnouncementTask>>? _$multicastDiscovery(ParentIsolateState v) => v.multicastDiscovery;
static const Field<ParentIsolateState, IsolateConnector<Device, SendToIsolateData<MulticastAnnouncementTask>>> _f$multicastDiscovery =
static IsolateConnector<Device, SendToIsolateData<MulticastTask>>? _$multicastDiscovery(ParentIsolateState v) => v.multicastDiscovery;
static const Field<ParentIsolateState, IsolateConnector<Device, SendToIsolateData<MulticastTask>>> _f$multicastDiscovery =
Field('multicastDiscovery', _$multicastDiscovery);
static List<IsolateConnector<IsolateTaskStreamResult<double>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>> _$httpUpload(
ParentIsolateState v) =>
@@ -114,7 +114,7 @@ abstract class ParentIsolateStateCopyWith<$R, $In extends ParentIsolateState, $O
{SyncState? syncState,
IsolateConnector<IsolateTaskStreamResult<Device>, SendToIsolateData<IsolateTask<HttpScanTask>>>? httpScanDiscovery,
IsolateConnector<IsolateTaskResult<Device>, SendToIsolateData<IsolateTask<HttpTargetTask>>>? httpTargetDiscovery,
IsolateConnector<Device, SendToIsolateData<MulticastAnnouncementTask>>? multicastDiscovery,
IsolateConnector<Device, SendToIsolateData<MulticastTask>>? multicastDiscovery,
List<IsolateConnector<IsolateTaskStreamResult<double>, SendToIsolateData<IsolateTask<BaseHttpUploadTask>>>>? httpUpload});
ParentIsolateStateCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
@@ -9,6 +9,7 @@ import 'package:common/model/device.dart';
import 'package:common/model/dto/multicast_dto.dart';
import 'package:common/model/dto/register_dto.dart';
import 'package:common/src/isolate/child/http_provider.dart';
import 'package:common/util/network_interfaces.dart';
import 'package:common/util/sleep.dart';
import 'package:logging/logging.dart';
import 'package:refena/refena.dart';
@@ -23,6 +24,7 @@ class MulticastService {
MulticastService(this._ref);
final Ref _ref;
Completer<void> _cancelCompleter = Completer();
bool _listening = false;
/// Binds the UDP port and listen to UDP multicast packages
@@ -35,10 +37,16 @@ class MulticastService {
_listening = true;
while (true) {
final streamController = StreamController<Device>();
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(syncState.multicastGroup, syncState.port);
final sockets = await _getSockets(
whitelist: syncState.networkWhitelist,
blacklist: syncState.networkBlacklist,
multicastGroup: syncState.multicastGroup,
port: syncState.port,
);
for (final socket in sockets) {
socket.socket.listen((_) {
final datagram = socket.socket.receive();
@@ -71,13 +79,36 @@ class MulticastService {
// Tell everyone in the network that I am online
sendAnnouncement(); // ignore: unawaited_futures
_cancelCompleter = Completer();
// ignore: unawaited_futures
_cancelCompleter.future.then((_) {
streamController.close();
for (final socket in sockets) {
socket.socket.close();
}
});
yield* streamController.stream;
// streamController is closed because of cancel
// wait for resources to be released (it works without on macOS, but who knows)
await sleepAsync(500);
}
}
void restartListener() {
_cancelCompleter.complete();
}
/// Sends an announcement which triggers a response on every LocalSend member of the network.
Future<void> sendAnnouncement() async {
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(syncState.multicastGroup);
final sockets = await _getSockets(
whitelist: syncState.networkWhitelist,
blacklist: syncState.networkBlacklist,
multicastGroup: syncState.multicastGroup,
);
final dto = _getMulticastDto(announcement: true);
for (final wait in [100, 500, 2000]) {
await sleepAsync(wait);
@@ -106,7 +137,11 @@ class MulticastService {
} catch (e) {
// Fallback: Answer with UDP
final syncState = _ref.read(syncProvider);
final sockets = await _getSockets(syncState.multicastGroup);
final sockets = await _getSockets(
whitelist: syncState.networkWhitelist,
blacklist: syncState.networkBlacklist,
multicastGroup: syncState.multicastGroup,
);
final dto = _getMulticastDto(announcement: false);
for (final socket in sockets) {
try {
@@ -160,8 +195,16 @@ class _SocketResult {
_SocketResult(this.interface, this.socket);
}
Future<List<_SocketResult>> _getSockets(String multicastGroup, [int? port]) async {
final interfaces = await NetworkInterface.list();
Future<List<_SocketResult>> _getSockets({
required List<String>? whitelist,
required List<String>? blacklist,
required String multicastGroup,
int? port,
}) async {
final interfaces = await getNetworkInterfaces(
whitelist: whitelist,
blacklist: blacklist,
);
final sockets = <_SocketResult>[];
for (final interface in interfaces) {
try {
+68
View File
@@ -0,0 +1,68 @@
import 'dart:io';
import 'package:logging/logging.dart';
final _logger = Logger('NetworkInterface');
/// Returns a list of network interfaces respecting the whitelist and blacklist.
Future<List<NetworkInterface>> getNetworkInterfaces({
required List<String>? whitelist,
required List<String>? blacklist,
}) async {
final result = <NetworkInterface>[];
final digestedWhitelist = whitelist?.map(buildRegExpFromIpFilter).toList();
final digestedBlacklist = blacklist?.map(buildRegExpFromIpFilter).toList();
for (final interface in await NetworkInterface.list()) {
if (isNetworkIgnored(
networkWhitelist: digestedWhitelist,
networkBlacklist: digestedBlacklist,
interface: interface.addresses.map((a) => a.address).toList(),
)) {
_logger.info('Ignore network interface ${interface.name} (${interface.addresses.map((a) => a.address).toList()})');
continue;
}
result.add(interface);
}
return result;
}
/// Returns true if the given IP should be ignored.
/// - When the IP is not in the whitelist (if the whitelist is not null)
/// - When the IP is in the blacklist (if the blacklist is not null)
bool isNetworkIgnoredRaw({
required List<String>? networkWhitelist,
required List<String>? networkBlacklist,
required List<String> interface,
}) {
return isNetworkIgnored(
networkWhitelist: networkWhitelist?.map(buildRegExpFromIpFilter).toList(),
networkBlacklist: networkBlacklist?.map(buildRegExpFromIpFilter).toList(),
interface: interface,
);
}
/// Builds a regular expression from the given IP.
/// - '123.123.124.*' -> '^123\.123\.124\.[^.]+$'
/// - '1::1:*:3' -> '^1::1:[^.]+:3$'
RegExp buildRegExpFromIpFilter(String ip) {
return RegExp('^${ip.replaceAll('.', '\\.').replaceAll('*', '[^.]+')}\$');
}
/// Returns true if the given IP should be ignored.
/// - When the IP is not in the whitelist (if the whitelist is not null)
/// - When the IP is in the blacklist (if the blacklist is not null)
bool isNetworkIgnored({
required List<RegExp>? networkWhitelist,
required List<RegExp>? networkBlacklist,
required List<String> interface,
}) {
if (networkWhitelist != null && !interface.any((a) => networkWhitelist.any((w) => w.hasMatch(a)))) {
return true;
}
if (networkBlacklist != null && interface.any((a) => networkBlacklist.any((b) => b.hasMatch(a)))) {
return true;
}
return false;
}
@@ -0,0 +1,49 @@
import 'package:common/util/network_interfaces.dart';
import 'package:test/test.dart';
void main() {
test('Should allow any IP', () {
ignored(String ip) => isNetworkIgnoredRaw(networkWhitelist: null, networkBlacklist: null, interface: [ip]);
expect(ignored('1.2.3.4'), false);
expect(ignored('1::1'), false);
});
test('Should only allow explicit whitelisted IP', () {
ignored(String ip) => isNetworkIgnoredRaw(networkWhitelist: ['2.2.2.3'], networkBlacklist: null, interface: [ip]);
expect(ignored('2.2.2.2'), true);
expect(ignored('2.2.2.3'), false);
});
test('Should allow wildcard whitelisted IP', () {
ignored(String ip) => isNetworkIgnoredRaw(networkWhitelist: ['3.3.3.*'], networkBlacklist: null, interface: [ip]);
expect(ignored('3.3.3.1'), false);
expect(ignored('3.3.3.2'), false);
expect(ignored('3.3.4.3'), true);
});
test('Should allow wildcard whitelisted ipv6', () {
ignored(String ip) => isNetworkIgnoredRaw(networkWhitelist: ['1::1:*'], networkBlacklist: null, interface: [ip]);
expect(ignored('1::1:1'), false);
expect(ignored('1::1:2'), false);
expect(ignored('1::2:1'), true);
});
test('Should ignore explicit blacklisted IP', () {
ignored(String ip) => isNetworkIgnoredRaw(networkWhitelist: null, networkBlacklist: ['3.3.3.4'], interface: [ip]);
expect(ignored('3.3.3.3'), false);
expect(ignored('3.3.3.4'), true);
});
test('Should ignore wildcard blacklisted IP', () {
ignored(String ip) => isNetworkIgnoredRaw(networkWhitelist: null, networkBlacklist: ['3.3.3.*'], interface: [ip]);
expect(ignored('3.3.3.3'), true);
expect(ignored('3.3.3.4'), true);
expect(ignored('4.3.3.3'), false);
});
}