Added rate limit whitelist

This commit is contained in:
Juan Gilsanz Polo 2024-02-03 19:56:27 +01:00
parent 8df0781da6
commit 1bc1efbe56
6 changed files with 240 additions and 5 deletions

View file

@ -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"
}

View file

@ -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"
}

View file

@ -25,6 +25,7 @@ class DnsInfo {
int? blockedResponseTtl;
int? ratelimitSubnetLenIpv4;
int? ratelimitSubnetLenIpv6;
List<String>? 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<String, dynamic> 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<String>.from(json["ratelimit_whitelist"].map((x) => x)) : [],
);
Map<String, dynamic> 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<String>.from(ratelimitWhitelist!.map((x) => x)) : null,
};
}

View file

@ -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;
}

View file

@ -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<DnsServerSettingsScreen> {
String? _ipv4PrefixSubnetError;
final _ipv6PrefixSubnetController = TextEditingController();
String? _ipv6PrefixSubnetError;
List<String> _rateLimitAllowlist = [];
String? _limitRequestsError;
final _expandableCustomEdns = ExpandableController();
final _expandableEdnsIp = ExpandableController();
@ -147,6 +150,7 @@ class _DnsServerSettingsScreenState extends State<DnsServerSettingsScreen> {
_ipv6PrefixSubnetController.text = dnsProvider.dnsInfo!.ratelimitSubnetLenIpv6 != null
? dnsProvider.dnsInfo!.ratelimitSubnetLenIpv6.toString()
: "";
_rateLimitAllowlist = dnsProvider.dnsInfo!.ratelimitWhitelist ?? [];
super.initState();
}
@ -173,6 +177,7 @@ class _DnsServerSettingsScreenState extends State<DnsServerSettingsScreen> {
"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<DnsServerSettingsScreen> {
),
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<DnsServerSettingsScreen> {
),
),
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<DnsServerSettingsScreen> {
),
),
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(() {

View file

@ -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<String> values;
final void Function(List<String>) onConfirm;
const RateLimitAllowlistModal({
super.key,
required this.values,
required this.onConfirm,
});
@override
State<RateLimitAllowlistModal> createState() => _RateLimitAllowlistModalState();
}
class _RateLimitAllowlistModalState extends State<RateLimitAllowlistModal> {
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)
),
],
)
],
),
),
),
);
}
}