Created config encryption UI

This commit is contained in:
Juan Gilsanz Polo 2022-10-22 23:06:27 +02:00
parent e0edcb67fa
commit 137a976a36
7 changed files with 687 additions and 5 deletions

View file

@ -490,5 +490,30 @@
"dnsCacheConfigDescription": "Configure how the server should manage the DNS cache",
"comment": "Comment",
"address": "Address",
"commentsDescription": "Comments are always preceded by #. You don't have to add it, it will be added automatically."
"commentsDescription": "Comments are always preceded by #. You don't have to add it, it will be added automatically.",
"encryptionSettings": "Encryption settings",
"encryptionSettingsDescription": "Encryption (HTTPS/QUIC/TLS) support",
"loadingEncryptionSettings": "Loading encryption settings...",
"encryptionSettingsNotLoaded": "Encryption settings couldn't be loaded.",
"enableEncryption": "Enable encryption",
"enableEncryptionTypes": "HTTPS, DNS-over-HTTPS, and DNS-over-TLS",
"enableEncryptionDescription": "If encryption is enabled, AdGuard Home admin interface will work over HTTPS, and the DNS server will listen for requests over DNS-over-HTTPS and DNS-over-TLS.",
"serverConfiguration": "Server configuration",
"domainName": "Domain name",
"domainNameDescription": "If set, AdGuard Home detects ClientIDs, responds to DDR queries, and performs additional connection validations. If not set, these features are disabled. Must match one of the DNS Names in the certificate.",
"redirectHttps": "Redirect to HTTPS automatically",
"httpsPort": "HTTPS port",
"tlsPort": "DNS-over-TLS port",
"dnsOverQuicPort": "DNS-over-QUIC port",
"certificates": "Certificates",
"certificatesDescription": "In order to use encryption, you need to provide a valid SSL certificates chain for your domain. You can get a free certificate on letsencrypt.org or you can buy it from one of the trusted Certificate Authorities.",
"certificateFilePath": "Set a certificates file path",
"pasteCertificateContent": "Paste the certificates contents",
"certificatePath": "Certificate path",
"certificateContent": "Certificate content",
"privateKey": "Private key",
"privateKeyFile": "Set a private key file",
"pastePrivateKey": "Paste the private key contents",
"usePreviousKey": "Use the previously saved key",
"privateKeyPath": "Private key path"
}

View file

@ -490,5 +490,30 @@
"dnsCacheConfigDescription": "Configura cómo el servidor debe manejar la caché del DNS",
"comment": "Comentario",
"address": "Dirección",
"commentsDescription": "Los comentarios siempre van precedidos por #. No necesitas añadirlo. Se añadirá automáticamente."
"commentsDescription": "Los comentarios siempre van precedidos por #. No necesitas añadirlo. Se añadirá automáticamente.",
"encryptionSettings": "Configuración de cifrado",
"encryptionSettingsDescription": "Soporte de cifrado (HTTPS/QUIC/TLS)",
"loadingEncryptionSettings": "Cargando configuración de cifrado...",
"encryptionSettingsNotLoaded": "No se ha podido cargar la configuración de cifrado.",
"enableEncryption": "Habilitar cifrado",
"enableEncryptionTypes": "HTTPS, DNS mediante HTTPS y DNS mediante TLS",
"enableEncryptionDescription": "Si el cifrado está habilitado, la interfaz de administración de AdGuard Home funcionará a través de HTTPS, y el servidor DNS escuchará las peticiones DNS mediante HTTPS y DNS mediante TLS.",
"serverConfiguration": "Configuración del servidor",
"domainName": "Nombre de dominio",
"domainNameDescription": "Si se configura, AdGuard Home detecta los ID de clientes, responde a las consultas DDR y realiza validaciones de conexión adicionales. Si no se configura, estas funciones se deshabilitarán. Debe coincidir con uno de los nombres DNS del certificado.",
"redirectHttps": "Redireccionar automáticamente a HTTPS",
"httpsPort": "Puerto HTTPS",
"tlsPort": "Puerto DNS sobre TLS",
"dnsOverQuicPort": "Puerto DNS sobre QUIC",
"certificates": "Certificados",
"certificatesDescription": "Para utilizar el cifrado, debes proporcionar una cadena de certificado SSL válida para tu dominio. Puedes obtener un certificado gratuito en letsencrypt.org o puedes comprarlo en una de las autoridades de certificación de confianza.",
"certificateFilePath": "Establecer una ruta para el archivo de certificado",
"pasteCertificateContent": "Pegar el contenido del certificado",
"certificatePath": "Ruta de acceso al certificado",
"certificateContent": "Contenido del certificado",
"privateKey": "Clave privada",
"privateKeyFile": "Establecer un archivo de clave privada",
"pastePrivateKey": "Pegar el contenido de la clave privada",
"usePreviousKey": "Usar la clave privada guardada previamente",
"privateKeyPath": "Ruta de la clave privada"
}

108
lib/models/encryption.dart Normal file
View file

@ -0,0 +1,108 @@
import 'dart:convert';
class Encryption {
int loadStatus = 0;
EncryptionData? data;
Encryption({
required this.loadStatus,
this.data
});
}
EncryptionData encryptionDataFromJson(String str) => EncryptionData.fromJson(json.decode(str));
String encryptionDataToJson(EncryptionData data) => json.encode(data.toJson());
class EncryptionData {
final bool validCert;
final bool validChain;
final DateTime notBefore;
final DateTime notAfter;
final dynamic dnsNames;
final bool validKey;
final bool validPair;
final bool enabled;
final bool forceHttps;
final int portHttps;
final int portDnsOverTls;
final int portDnsOverQuic;
final int portDnscrypt;
final String dnscryptConfigFile;
final bool allowUnencryptedDoh;
final String certificateChain;
final String privateKey;
final String certificatePath;
final String privateKeyPath;
final bool privateKeySaved;
EncryptionData({
required this.validCert,
required this.validChain,
required this.notBefore,
required this.notAfter,
required this.dnsNames,
required this.validKey,
required this.validPair,
required this.enabled,
required this.forceHttps,
required this.portHttps,
required this.portDnsOverTls,
required this.portDnsOverQuic,
required this.portDnscrypt,
required this.dnscryptConfigFile,
required this.allowUnencryptedDoh,
required this.certificateChain,
required this.privateKey,
required this.certificatePath,
required this.privateKeyPath,
required this.privateKeySaved,
});
factory EncryptionData.fromJson(Map<String, dynamic> json) => EncryptionData(
validCert: json["valid_cert"],
validChain: json["valid_chain"],
notBefore: DateTime.parse(json["not_before"]),
notAfter: DateTime.parse(json["not_after"]),
dnsNames: json["dns_names"],
validKey: json["valid_key"],
validPair: json["valid_pair"],
enabled: json["enabled"],
forceHttps: json["force_https"],
portHttps: json["port_https"],
portDnsOverTls: json["port_dns_over_tls"],
portDnsOverQuic: json["port_dns_over_quic"],
portDnscrypt: json["port_dnscrypt"],
dnscryptConfigFile: json["dnscrypt_config_file"],
allowUnencryptedDoh: json["allow_unencrypted_doh"],
certificateChain: json["certificate_chain"],
privateKey: json["private_key"],
certificatePath: json["certificate_path"],
privateKeyPath: json["private_key_path"],
privateKeySaved: json["private_key_saved"],
);
Map<String, dynamic> toJson() => {
"valid_cert": validCert,
"valid_chain": validChain,
"not_before": notBefore.toIso8601String(),
"not_after": notAfter.toIso8601String(),
"dns_names": dnsNames,
"valid_key": validKey,
"valid_pair": validPair,
"enabled": enabled,
"force_https": forceHttps,
"port_https": portHttps,
"port_dns_over_tls": portDnsOverTls,
"port_dns_over_quic": portDnsOverQuic,
"port_dnscrypt": portDnscrypt,
"dnscrypt_config_file": dnscryptConfigFile,
"allow_unencrypted_doh": allowUnencryptedDoh,
"certificate_chain": certificateChain,
"private_key": privateKey,
"certificate_path": certificatePath,
"private_key_path": privateKeyPath,
"private_key_saved": privateKeySaved,
};
}

View file

@ -7,11 +7,12 @@ import 'package:adguard_home_manager/models/dns_info.dart';
import 'package:adguard_home_manager/models/rewrite_rules.dart';
import 'package:adguard_home_manager/models/filtering_status.dart';
import 'package:adguard_home_manager/models/clients_allowed_blocked.dart';
import 'package:adguard_home_manager/models/encryption.dart';
import 'package:adguard_home_manager/models/clients.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/models/server_status.dart';
import 'package:adguard_home_manager/functions/conversions.dart';
import 'package:adguard_home_manager/models/server.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/functions/conversions.dart';
class ServersProvider with ChangeNotifier {
Database? _dbInstance;
@ -49,6 +50,11 @@ class ServersProvider with ChangeNotifier {
data: null
);
final Encryption _encryptionSettings = Encryption(
loadStatus: 0, // 0 = loading, 1 = loaded, 2 = error,
data: null
);
FilteringStatus? _filteringStatus;
List<Server> get serversList {
@ -91,6 +97,10 @@ class ServersProvider with ChangeNotifier {
return _dnsInfo;
}
Encryption get encryptionSettings {
return _encryptionSettings;
}
void setDbInstance(Database db) {
_dbInstance = db;
}
@ -201,6 +211,18 @@ class ServersProvider with ChangeNotifier {
}
}
void setEncryptionSettings(EncryptionData data) {
_encryptionSettings.data = data;
notifyListeners();
}
void setEncryptionSettingsLoadStatus(int status, bool notify) {
_encryptionSettings.loadStatus = status;
if (notify == true) {
notifyListeners();
}
}
Future<bool> createServer(Server server) async {
final saved = await saveServerIntoDb(server);
if (saved == true) {

View file

@ -0,0 +1,452 @@
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/section_label.dart';
import 'package:adguard_home_manager/widgets/custom_switch_list_tile.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class EncryptionSettings extends StatelessWidget {
const EncryptionSettings({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
return EncryptionSettingsWidget(
serversProvider: serversProvider,
appConfigProvider: appConfigProvider,
);
}
}
class EncryptionSettingsWidget extends StatefulWidget {
final ServersProvider serversProvider;
final AppConfigProvider appConfigProvider;
const EncryptionSettingsWidget({
Key? key,
required this.serversProvider,
required this.appConfigProvider,
}) : super(key: key);
@override
State<EncryptionSettingsWidget> createState() => _EncryptionSettingsWidgetState();
}
class _EncryptionSettingsWidgetState extends State<EncryptionSettingsWidget> {
bool enabled = false;
final TextEditingController domainNameController = TextEditingController();
String? domainError;
bool redirectHttps = false;
final TextEditingController httpsPortController = TextEditingController();
String? httpsPortError;
final TextEditingController tlsPortController = TextEditingController();
String? tlsPortError;
final TextEditingController dnsOverQuicPortController = TextEditingController();
String? dnsOverQuicPortError;
int certificateOption = 0;
final TextEditingController certificatePathController = TextEditingController();
String? certificatePathError;
final TextEditingController certificateContentController = TextEditingController();
String? certificateContentError;
int privateKeyOption = 0;
bool usePreviouslySavedKey = false;
final TextEditingController privateKeyPathController = TextEditingController();
String? privateKeyPathError;
final TextEditingController pastePrivateKeyController = TextEditingController();
String? pastePrivateKeyError;
void fetchData({bool? showRefreshIndicator}) async {
widget.serversProvider.setEncryptionSettingsLoadStatus(0, showRefreshIndicator ?? false);
final result = await getEncryptionSettings(server: widget.serversProvider.selectedServer!);
if (mounted) {
if (result['result'] == 'success') {
widget.serversProvider.setEncryptionSettings(result['data']);
widget.serversProvider.setEncryptionSettingsLoadStatus(1, true);
}
else {
widget.appConfigProvider.addLog(result['log']);
widget.serversProvider.setEncryptionSettingsLoadStatus(2, true);
}
}
}
@override
void initState() {
fetchData();
super.initState();
}
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
Widget generateBody() {
switch (widget.serversProvider.encryptionSettings.loadStatus) {
case 0:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.loadingEncryptionSettings,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 22,
color: Colors.grey,
),
)
],
)
);
case 1:
return ListView(
children: [
Padding(
padding: const EdgeInsets.only(
top: 10,
left: 12,
right: 12
),
child: Material(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(28),
child: InkWell(
onTap: () => setState(() => enabled = !enabled),
borderRadius: BorderRadius.circular(28),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.enableEncryption,
style: const TextStyle(
fontSize: 18,
),
),
const SizedBox(height: 3),
Text(
AppLocalizations.of(context)!.enableEncryptionTypes,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).listTileTheme.iconColor,
),
)
],
),
),
Switch(
value: enabled,
onChanged: (value) => setState(() => enabled = value),
activeColor: Theme.of(context).primaryColor,
),
],
),
),
),
),
),
SectionLabel(label: AppLocalizations.of(context)!.serverConfiguration),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: domainNameController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.link_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: domainError,
labelText: AppLocalizations.of(context)!.domainName,
helperText: AppLocalizations.of(context)!.domainNameDescription,
helperMaxLines: 10
),
),
),
const SizedBox(height: 10),
CustomSwitchListTile(
value: redirectHttps,
onChanged: (value) => setState(() => redirectHttps = value),
title: AppLocalizations.of(context)!.redirectHttps,
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: httpsPortController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.numbers_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: httpsPortError,
labelText: AppLocalizations.of(context)!.httpsPort,
),
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: tlsPortController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.numbers_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: tlsPortError,
labelText: AppLocalizations.of(context)!.tlsPort,
),
keyboardType: TextInputType.number,
),
),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: dnsOverQuicPortController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.numbers_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: dnsOverQuicPortError,
labelText: AppLocalizations.of(context)!.dnsOverQuicPort,
),
keyboardType: TextInputType.number,
),
),
SectionLabel(label: AppLocalizations.of(context)!.certificates),
Card(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
const Icon(Icons.info_rounded),
const SizedBox(width: 20),
Flexible(
child: Text(AppLocalizations.of(context)!.certificatesDescription)
)
],
),
),
),
const SizedBox(height: 20),
RadioListTile(
value: 0,
groupValue: certificateOption,
onChanged: (value) => setState(() => certificateOption = value!),
title: Text(
AppLocalizations.of(context)!.certificateFilePath,
style: const TextStyle(
fontWeight: FontWeight.normal
),
),
),
RadioListTile(
value: 1,
groupValue: certificateOption,
onChanged: (value) => setState(() => certificateOption = value!),
title: Text(
AppLocalizations.of(context)!.pasteCertificateContent,
style: const TextStyle(
fontWeight: FontWeight.normal
),
),
),
const SizedBox(height: 10),
if (certificateOption == 0) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: certificatePathController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: certificatePathError,
labelText: AppLocalizations.of(context)!.certificatePath,
),
),
),
if (certificateOption == 1) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: certificateContentController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: certificateContentError,
labelText: AppLocalizations.of(context)!.certificateContent,
),
keyboardType: TextInputType.multiline,
maxLines: null,
),
),
SectionLabel(label: AppLocalizations.of(context)!.privateKey),
RadioListTile(
value: 0,
groupValue: privateKeyOption,
onChanged: (value) => setState(() => privateKeyOption = value!),
title: Text(
AppLocalizations.of(context)!.privateKeyFile,
style: const TextStyle(
fontWeight: FontWeight.normal
),
),
),
RadioListTile(
value: 1,
groupValue: privateKeyOption,
onChanged: (value) => setState(() => privateKeyOption = value!),
title: Text(
AppLocalizations.of(context)!.pastePrivateKey,
style: const TextStyle(
fontWeight: FontWeight.normal
),
),
),
if (privateKeyOption == 0) const SizedBox(height: 10),
if (privateKeyOption == 1) ...[
CustomSwitchListTile(
value: usePreviouslySavedKey,
onChanged: (value) => setState(() => usePreviouslySavedKey = value),
title: AppLocalizations.of(context)!.usePreviousKey,
),
const SizedBox(height: 10)
],
if (privateKeyOption == 0) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
controller: privateKeyPathController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: privateKeyPathError,
labelText: AppLocalizations.of(context)!.privateKeyPath,
),
),
),
if (privateKeyOption == 1) Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextFormField(
enabled: !usePreviouslySavedKey,
controller: pastePrivateKeyController,
// onChanged:
decoration: InputDecoration(
prefixIcon: const Icon(Icons.description_rounded),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10)
)
),
errorText: pastePrivateKeyError,
labelText: AppLocalizations.of(context)!.pastePrivateKey,
),
keyboardType: TextInputType.multiline,
maxLines: null,
),
),
const SizedBox(height: 20),
],
);
case 2:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.encryptionSettingsNotLoaded,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 22,
color: Colors.grey,
),
)
],
),
);
default:
return const SizedBox();
}
}
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.encryptionSettings),
),
body: generateBody(),
);
}
}

View file

@ -6,7 +6,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/settings/theme_modal.dart';
import 'package:adguard_home_manager/screens/settings/server_info/server_info.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/screens/settings/encryption/encryption.dart';
import 'package:adguard_home_manager/screens/settings/access_settings/access_settings.dart';
import 'package:adguard_home_manager/screens/settings/dhcp/dhcp.dart';
import 'package:adguard_home_manager/screens/settings/section_label.dart';
@ -17,6 +17,8 @@ import 'package:adguard_home_manager/screens/servers/servers.dart';
import 'package:adguard_home_manager/screens/settings/advanced_setings.dart';
import 'package:adguard_home_manager/screens/settings/general_settings.dart';
import 'package:adguard_home_manager/widgets/custom_list_tile.dart';
import 'package:adguard_home_manager/constants/strings.dart';
import 'package:adguard_home_manager/constants/urls.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
@ -126,6 +128,18 @@ class Settings extends StatelessWidget {
)
},
),
CustomListTile(
icon: Icons.security_rounded,
title: AppLocalizations.of(context)!.encryptionSettings,
subtitle: AppLocalizations.of(context)!.encryptionSettingsDescription,
onTap: () => {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const EncryptionSettings()
)
)
},
),
CustomListTile(
icon: Icons.route_rounded,
title: AppLocalizations.of(context)!.dnsRewrites,

View file

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:adguard_home_manager/models/dhcp.dart';
import 'package:adguard_home_manager/models/dns_info.dart';
import 'package:adguard_home_manager/models/encryption.dart';
import 'package:adguard_home_manager/models/filtering.dart';
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/models/filtering_status.dart';
@ -1704,4 +1705,39 @@ Future setDnsConfig({
else {
return result;
}
}
Future getEncryptionSettings({
required Server server,
}) async {
final result = await apiRequest(
urlPath: '/tls/status',
method: 'get',
server: server,
type: 'get_encryption_settings'
);
if (result['hasResponse'] == true) {
if (result['statusCode'] == 200) {
return {
'result': 'success',
'data': EncryptionData.fromJson(jsonDecode(result['body']))
};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'get_encryption_settings',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result['statusCode'].toString(),
resBody: result['body'],
)
};
}
}
else {
return result;
}
}