From 27e0b5152ec065a297646b26c840df9b360eb3f2 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Mon, 11 Dec 2023 22:03:36 +0100 Subject: [PATCH] Added fallback DNS servers --- lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 6 +- lib/models/dns_info.dart | 4 + lib/providers/dns_provider.dart | 16 ++ lib/screens/settings/dns/dns.dart | 7 + lib/screens/settings/dns/fallback_dns.dart | 224 +++++++++++++++++++++ 6 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 lib/screens/settings/dns/fallback_dns.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1a4cdb4..7fd17e2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -706,5 +706,9 @@ "queryLogsAndStatistics": "Query logs and statistics", "ignoreClientQueryLog": "Ignore this client in query log", "ignoreClientStatistics": "Ignore this client in statistics", - "savingChanges": "Saving changes..." + "savingChanges": "Saving changes...", + "fallbackDnsServers": "Fallback DNS servers", + "fallbackDnsServersDescription": "Configure fallback DNS servers", + "fallbackDnsServersInfo": "List of fallback DNS servers used when upstream DNS servers are not responding. The syntax is the same as in the main upstreams field above.", + "noFallbackDnsAdded": "No fallback DNS servers added." } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e0e57c1..33b69b1 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -706,5 +706,9 @@ "queryLogsAndStatistics": "Registro de consultas y estadísticas", "ignoreClientQueryLog": "Ignorar este cliente en el registro de consultas", "ignoreClientStatistics": "Ignorar este cliente en las estadísticas", - "savingChanges": "Guardando cambios..." + "savingChanges": "Guardando cambios...", + "fallbackDnsServers": "Servidores DNS alternativos", + "fallbackDnsServersDescription": "Configura los servidores DNS alternativos", + "fallbackDnsServersInfo": "Lista de servidores DNS alternativos utilizados cuando los servidores DNS de subida no responden. La sintaxis es la misma que en el campo de los principales DNS de subida anterior.", + "noFallbackDnsAdded": "No hay servidores DNS alternativos añadidos." } \ No newline at end of file diff --git a/lib/models/dns_info.dart b/lib/models/dns_info.dart index 0d3aa83..20338d3 100644 --- a/lib/models/dns_info.dart +++ b/lib/models/dns_info.dart @@ -2,6 +2,7 @@ class DnsInfo { List upstreamDns; String? upstreamDnsFile; List bootstrapDns; + List? fallbackDns; bool protectionEnabled; int ratelimit; String blockingMode; @@ -26,6 +27,7 @@ class DnsInfo { required this.upstreamDns, required this.upstreamDnsFile, required this.bootstrapDns, + required this.fallbackDns, required this.protectionEnabled, required this.ratelimit, required this.blockingMode, @@ -51,6 +53,7 @@ class DnsInfo { upstreamDns: json["upstream_dns"] != null ? List.from(json["upstream_dns"].map((x) => x)) : [], upstreamDnsFile: json["upstream_dns_file"], bootstrapDns: json["bootstrap_dns"] != null ? List.from(json["bootstrap_dns"].map((x) => x)) : [], + fallbackDns: json["fallback_dns"] != null ? List.from(json["fallback_dns"].map((x) => x)) : [], protectionEnabled: json["protection_enabled"], ratelimit: json["ratelimit"], blockingMode: json["blocking_mode"], @@ -76,6 +79,7 @@ class DnsInfo { "upstream_dns": List.from(upstreamDns.map((x) => x)), "upstream_dns_file": upstreamDnsFile, "bootstrap_dns": List.from(bootstrapDns.map((x) => x)), + "fallback_dns": List.from(bootstrapDns.map((x) => x)), "protection_enabled": protectionEnabled, "ratelimit": ratelimit, "blocking_mode": blockingMode, diff --git a/lib/providers/dns_provider.dart b/lib/providers/dns_provider.dart index 0929f3a..5a91dfc 100644 --- a/lib/providers/dns_provider.dart +++ b/lib/providers/dns_provider.dart @@ -112,6 +112,22 @@ class DnsProvider with ChangeNotifier { } } + Future saveFallbackDnsConfig(Map value) async { + final result = await _serversProvider!.apiClient2!.setDnsConfig( + data: value + ); + + if (result.successful == true) { + DnsInfo data = dnsInfo!; + data.bootstrapDns = List.from(value['fallback_dns']); + setDnsInfoData(data); + return result; + } + else { + return result; + } + } + Future saveCacheCacheConfig(Map value) async { final result = await _serversProvider!.apiClient2!.setDnsConfig( data: value diff --git a/lib/screens/settings/dns/dns.dart b/lib/screens/settings/dns/dns.dart index c22572d..4a7c78a 100644 --- a/lib/screens/settings/dns/dns.dart +++ b/lib/screens/settings/dns/dns.dart @@ -5,6 +5,7 @@ import 'package:flutter_split_view/flutter_split_view.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/settings/dns/fallback_dns.dart'; import 'package:adguard_home_manager/screens/settings/dns/test_upstream_dns_modal.dart'; import 'package:adguard_home_manager/screens/settings/dns/clear_dns_cache_dialog.dart'; import 'package:adguard_home_manager/screens/settings/dns/cache_config.dart'; @@ -167,6 +168,12 @@ class _DnsSettingsState extends State { onTap: () => navigate(const BootstrapDnsScreen()), icon: Icons.dns_rounded, ), + if (dnsProvider.dnsInfo!.fallbackDns != null) CustomListTile( + title: AppLocalizations.of(context)!.fallbackDnsServers, + subtitle: AppLocalizations.of(context)!.fallbackDnsServersDescription, + onTap: () => navigate(const FallbackDnsScreen()), + icon: Icons.alt_route_rounded, + ), CustomListTile( title: AppLocalizations.of(context)!.privateReverseDnsServers, subtitle: AppLocalizations.of(context)!.privateReverseDnsDescription, diff --git a/lib/screens/settings/dns/fallback_dns.dart b/lib/screens/settings/dns/fallback_dns.dart new file mode 100644 index 0000000..4df30c9 --- /dev/null +++ b/lib/screens/settings/dns/fallback_dns.dart @@ -0,0 +1,224 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:adguard_home_manager/functions/desktop_mode.dart'; +import 'package:adguard_home_manager/classes/process_modal.dart'; +import 'package:adguard_home_manager/providers/dns_provider.dart'; +import 'package:adguard_home_manager/functions/snackbar.dart'; +import 'package:adguard_home_manager/providers/app_config_provider.dart'; + +class FallbackDnsScreen extends StatefulWidget { + const FallbackDnsScreen({super.key}); + + @override + State createState() => _FallbackDnsScreenState(); +} + +class _FallbackDnsScreenState extends State { + List> fallbackControllers = []; + + bool validValues = false; + + void validateIp(Map field, String value) { + RegExp ipAddress = RegExp(r'(?:^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$)|(?:^(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?$)'); + if (ipAddress.hasMatch(value) == true) { + setState(() => field['error'] = null); + } + else { + setState(() => field['error'] = AppLocalizations.of(context)!.invalidIp); + } + checkValidValues(); + } + + void checkValidValues() { + if ( + fallbackControllers.isNotEmpty && + fallbackControllers.every((element) => element['controller'].text != '') && + fallbackControllers.every((element) => element['error'] == null) + ) { + setState(() => validValues = true); + } + else { + setState(() => validValues = false); + } + } + + @override + void initState() { + final dnsProvider = Provider.of(context, listen: false); + + for (var item in dnsProvider.dnsInfo!.fallbackDns!) { + final controller = TextEditingController(); + controller.text = item; + fallbackControllers.add({ + 'controller': controller, + 'error': null + }); + } + validValues = true; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final dnsProvider = Provider.of(context); + final appConfigProvider = Provider.of(context); + + final width = MediaQuery.of(context).size.width; + + void saveData() async { + ProcessModal processModal = ProcessModal(); + processModal.open(AppLocalizations.of(context)!.savingConfig); + + final result = await dnsProvider.saveFallbackDnsConfig({ + "fallback_dns": fallbackControllers.map((e) => e['controller'].text).toList(), + }); + + processModal.close(); + + if (result.successful == true) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsConfigSaved, + color: Colors.green + ); + } + else if (result.successful == false && result.statusCode == 400) { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.someValueNotValid, + color: Colors.red + ); + } + else { + showSnacbkar( + appConfigProvider: appConfigProvider, + label: AppLocalizations.of(context)!.dnsConfigNotSaved, + color: Colors.red + ); + } + } + + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.fallbackDnsServers), + surfaceTintColor: isDesktop(width) ? Colors.transparent : null, + actions: [ + IconButton( + onPressed: validValues == true + ? () => saveData() + : null, + icon: const Icon(Icons.save_rounded), + tooltip: AppLocalizations.of(context)!.save, + ), + const SizedBox(width: 10) + ], + ), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only(top: 10), + children: [ + Card( + margin: const EdgeInsets.only( + left: 16, right: 16, bottom: 20 + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.info_rounded, + color: Theme.of(context).listTileTheme.iconColor, + ), + const SizedBox(width: 20), + Flexible( + child: Text( + AppLocalizations.of(context)!.fallbackDnsServersInfo, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface + ), + ) + ) + ], + ), + ), + ), + const SizedBox(height: 10), + if (fallbackControllers.isEmpty) Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + AppLocalizations.of(context)!.noFallbackDnsAdded, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 16 + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ...fallbackControllers.map((c) => Padding( + padding: const EdgeInsets.only( + left: 16, right: 6, bottom: 20 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextFormField( + controller: c['controller'], + onChanged: (value) => validateIp(c, value), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + errorText: c['error'], + labelText: AppLocalizations.of(context)!.dnsServer, + ) + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () { + setState(() => fallbackControllers = fallbackControllers.where((con) => con != c).toList()); + checkValidValues(); + }, + icon: const Icon(Icons.remove_circle_outline) + ) + ], + ), + )), + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton.icon( + onPressed: () { + setState(() => fallbackControllers.add({ + 'controller': TextEditingController(), + 'error': null + })); + checkValidValues(); + }, + icon: const Icon(Icons.add), + label: Text(AppLocalizations.of(context)!.addItem) + ), + ], + ), + const SizedBox(height: 20) + ], + ), + ), + ); + } +} \ No newline at end of file