From 1bc1efbe5686e548c7ee013109c7e77bd2b59889 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sat, 3 Feb 2024 19:56:27 +0100 Subject: [PATCH] Added rate limit whitelist --- lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 6 +- lib/models/dns_info.dart | 4 + lib/providers/dns_provider.dart | 1 + .../settings/dns/dns_server_settings.dart | 42 +++- .../dns/rate_limit_allowlist_modal.dart | 186 ++++++++++++++++++ 6 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 lib/screens/settings/dns/rate_limit_allowlist_modal.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ab2a561..238b380 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -759,6 +759,10 @@ "removeDomain": "Remove domain", "addDomain": "Add domain", "notLess1Hour": "Time cannot be less than 1 hour", + "rateLimit": "Rate limit", "subnetPrefixLengthIpv4": "Subnet prefix length for IPv4", - "subnetPrefixLengthIpv6": "Subnet prefix length for IPv6" + "subnetPrefixLengthIpv6": "Subnet prefix length for IPv6", + "rateLimitAllowlist": "Rate limit allowlist", + "rateLimitAllowlistDescription": "IP addresses excluded from rate limiting", + "dnsOptions": "DNS options" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 6a701d1..718a093 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -759,6 +759,10 @@ "removeDomain": "Eliminar dominio", "addDomain": "Añadir dominio", "notLess1Hour": "El tiempo no puede ser inferior a 1 hora", + "rateLimit": "Limitación de velocidad", "subnetPrefixLengthIpv4": "Longitud del prefijo de subred para IPv4", - "subnetPrefixLengthIpv6": "Longitud del prefijo de subred para IPv6" + "subnetPrefixLengthIpv6": "Longitud del prefijo de subred para IPv6", + "rateLimitAllowlist": "Lista de permitidos de limitación de velocidad", + "rateLimitAllowlistDescription": "Direcciones IP excluidas de la limitación de velocidad", + "dnsOptions": "Opciones de DNS" } \ No newline at end of file diff --git a/lib/models/dns_info.dart b/lib/models/dns_info.dart index d4152c8..5a53023 100644 --- a/lib/models/dns_info.dart +++ b/lib/models/dns_info.dart @@ -25,6 +25,7 @@ class DnsInfo { int? blockedResponseTtl; int? ratelimitSubnetLenIpv4; int? ratelimitSubnetLenIpv6; + List? ratelimitWhitelist; DnsInfo({ required this.upstreamDns, @@ -53,6 +54,7 @@ class DnsInfo { required this.blockedResponseTtl, required this.ratelimitSubnetLenIpv4, required this.ratelimitSubnetLenIpv6, + required this.ratelimitWhitelist, }); factory DnsInfo.fromJson(Map json) => DnsInfo( @@ -82,6 +84,7 @@ class DnsInfo { blockedResponseTtl: json["blocked_response_ttl"], ratelimitSubnetLenIpv4: json["ratelimit_subnet_len_ipv4"], ratelimitSubnetLenIpv6: json["ratelimit_subnet_len_ipv6"], + ratelimitWhitelist: json["ratelimit_whitelist"] != null ? List.from(json["ratelimit_whitelist"].map((x) => x)) : [], ); Map toJson() => { @@ -111,5 +114,6 @@ class DnsInfo { "blocked_response_ttl": blockedResponseTtl, "ratelimit_subnet_len_ipv4": ratelimitSubnetLenIpv4, "ratelimit_subnet_len_ipv6": ratelimitSubnetLenIpv6, + "ratelimit_whitelist": ratelimitWhitelist != null ? List.from(ratelimitWhitelist!.map((x) => x)) : null, }; } diff --git a/lib/providers/dns_provider.dart b/lib/providers/dns_provider.dart index 9c8d705..198053b 100644 --- a/lib/providers/dns_provider.dart +++ b/lib/providers/dns_provider.dart @@ -164,6 +164,7 @@ class DnsProvider with ChangeNotifier { data.blockedResponseTtl = value['blocked_response_ttl']; data.ratelimitSubnetLenIpv4 = value['ratelimit_subnet_len_ipv4']; data.ratelimitSubnetLenIpv6 = value['ratelimit_subnet_len_ipv6']; + data.ratelimitWhitelist = value['ratelimit_whitelist']; setDnsInfoData(data); return result; } diff --git a/lib/screens/settings/dns/dns_server_settings.dart b/lib/screens/settings/dns/dns_server_settings.dart index 6e747f3..ddae1e3 100644 --- a/lib/screens/settings/dns/dns_server_settings.dart +++ b/lib/screens/settings/dns/dns_server_settings.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:adguard_home_manager/screens/settings/dns/rate_limit_allowlist_modal.dart'; +import 'package:adguard_home_manager/widgets/custom_list_tile.dart'; import 'package:adguard_home_manager/widgets/custom_radio_list_tile.dart'; import 'package:adguard_home_manager/widgets/section_label.dart'; import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart'; @@ -28,6 +30,7 @@ class _DnsServerSettingsScreenState extends State { String? _ipv4PrefixSubnetError; final _ipv6PrefixSubnetController = TextEditingController(); String? _ipv6PrefixSubnetError; + List _rateLimitAllowlist = []; String? _limitRequestsError; final _expandableCustomEdns = ExpandableController(); final _expandableEdnsIp = ExpandableController(); @@ -147,6 +150,7 @@ class _DnsServerSettingsScreenState extends State { _ipv6PrefixSubnetController.text = dnsProvider.dnsInfo!.ratelimitSubnetLenIpv6 != null ? dnsProvider.dnsInfo!.ratelimitSubnetLenIpv6.toString() : ""; + _rateLimitAllowlist = dnsProvider.dnsInfo!.ratelimitWhitelist ?? []; super.initState(); } @@ -173,6 +177,7 @@ class _DnsServerSettingsScreenState extends State { "blocked_response_ttl": int.tryParse(_ttlController.text), "ratelimit_subnet_len_ipv4": int.tryParse(_ipv4PrefixSubnetController.text), "ratelimit_subnet_len_ipv6": int.tryParse(_ipv6PrefixSubnetController.text), + "ratelimit_whitelist": _rateLimitAllowlist }); processModal.close(); @@ -230,10 +235,14 @@ class _DnsServerSettingsScreenState extends State { ), body: SafeArea( child: ListView( - padding: const EdgeInsets.only(top: 10), + padding: const EdgeInsets.only(top: 0), children: [ + SectionLabel( + label: AppLocalizations.of(context)!.rateLimit, + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24, top: 8), + ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24, top: 4), child: TextFormField( controller: _limitRequestsController, onChanged: (v) => setState(() => _limitRequestsError = validateNumber(v)), @@ -251,7 +260,7 @@ class _DnsServerSettingsScreenState extends State { ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24), child: TextFormField( controller: _ipv4PrefixSubnetController, onChanged: (v) => setState(() => _ipv4PrefixSubnetError = validateNumber(v)), @@ -287,6 +296,33 @@ class _DnsServerSettingsScreenState extends State { ), ), const SizedBox(height: 16), + CustomListTile( + title: AppLocalizations.of(context)!.rateLimitAllowlist, + subtitle: AppLocalizations.of(context)!.rateLimitAllowlistDescription, + onTap: () => showDialog( + context: context, + builder: (context) => RateLimitAllowlistModal( + values: _rateLimitAllowlist, + onConfirm: (ips) => setState(() => _rateLimitAllowlist = ips) + ), + ), + trailing: _rateLimitAllowlist.isNotEmpty ? Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(40) + ), + child: Text( + _rateLimitAllowlist.length.toString(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onPrimaryContainer + ), + ), + ) : null, + ), + SectionLabel(label: AppLocalizations.of(context)!.dnsOptions), CustomSwitchListTile( value: _enableEdns, onChanged: (value) => setState(() { diff --git a/lib/screens/settings/dns/rate_limit_allowlist_modal.dart b/lib/screens/settings/dns/rate_limit_allowlist_modal.dart new file mode 100644 index 0000000..e5b678e --- /dev/null +++ b/lib/screens/settings/dns/rate_limit_allowlist_modal.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; +import 'package:uuid/uuid.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class _IpListItemController { + final String id; + final TextEditingController controller; + bool error; + + _IpListItemController({ + required this.id, + required this.controller, + required this.error + }); +} + +class RateLimitAllowlistModal extends StatefulWidget { + final List values; + final void Function(List) onConfirm; + + const RateLimitAllowlistModal({ + super.key, + required this.values, + required this.onConfirm, + }); + + @override + State createState() => _RateLimitAllowlistModalState(); +} + +class _RateLimitAllowlistModalState extends State { + final Uuid uuid = const Uuid(); + List<_IpListItemController> _controllersList = []; + + void _validateIp(String value, _IpListItemController item) { + final regexp = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); + if (regexp.hasMatch(value)) { + setState(() => _controllersList = _controllersList.map((e) { + if (e.id == item.id) { + return _IpListItemController( + id: e.id, + controller: e.controller, + error: false + ); + } + return e; + }).toList()); + } + else { + setState(() => _controllersList = _controllersList.map((e) { + if (e.id == item.id) { + return _IpListItemController( + id: e.id, + controller: e.controller, + error: true + ); + } + return e; + }).toList()); + } + } + + @override + void initState() { + _controllersList = widget.values.map((e) => _IpListItemController( + id: uuid.v4(), + controller: TextEditingController(text: e), + error: false + )).toList(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final validData = _controllersList.where((e) => e.error == true).isEmpty; + + return Dialog( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Icon( + Icons.check_circle_rounded, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context)!.rateLimitAllowlist, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + ), + ), + const SizedBox(height: 16), + ..._controllersList.map((item) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: item.controller, + onChanged: (v) => _validateIp(v, item), + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10) + ) + ), + labelText: AppLocalizations.of(context)!.ipAddress, + errorText: item.error == true + ? AppLocalizations.of(context)!.invalidIp + : null + ), + keyboardType: TextInputType.number, + ), + ), + const SizedBox(width: 16), + IconButton( + onPressed: () => setState( + () => _controllersList = _controllersList.where((c) => c.id != item.id).toList() + ), + icon: const Icon(Icons.remove_circle_outline_rounded), + tooltip: AppLocalizations.of(context)!.remove, + ) + ], + ), + )), + Center( + child: ElevatedButton.icon( + onPressed: () => setState(() => _controllersList.add( + _IpListItemController( + id: uuid.v4(), + controller: TextEditingController(), + error: false + ), + )), + icon: const Icon(Icons.add_rounded), + label: Text(AppLocalizations.of(context)!.addItem), + ), + ) + ], + ), + ) + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(AppLocalizations.of(context)!.cancel) + ), + const SizedBox(width: 8), + TextButton( + onPressed: validData == true + ? () { + widget.onConfirm( + _controllersList.map((e) => e.controller.text).toList() + ); + Navigator.pop(context); + } + : null, + child: Text(AppLocalizations.of(context)!.confirm) + ), + ], + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file