Merge pull request 'feat: Make DNS records deletion and creation dynamic' (#424) from dynamic-dns-deletion into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #424
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
pull/425/head
NaiJi ✨ 2024-01-19 15:46:58 +02:00
commit dca733f641
10 changed files with 115 additions and 76 deletions

1
devtools_options.yaml Normal file
View File

@ -0,0 +1 @@
extensions:

View File

@ -168,25 +168,19 @@ class DnsRecordsCubit
Future<void> fix() async { Future<void> fix() async {
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
final List<DnsRecord> records = await api.getDnsRecords();
/// TODO: Error handling?
final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
await ProvidersController.currentDnsProvider!.removeDomainRecords( await ProvidersController.currentDnsProvider!.removeDomainRecords(
records: records,
domain: domain!, domain: domain!,
); );
await ProvidersController.currentDnsProvider!.createDomainRecords( await ProvidersController.currentDnsProvider!.createDomainRecords(
records: records,
domain: domain, domain: domain,
ip4: ipAddress,
); );
final List<DnsRecord> records = await api.getDnsRecords();
final DnsRecord? dkimRecord = extractDkimRecord(records);
if (dkimRecord != null) {
await ProvidersController.currentDnsProvider!.setDnsRecord(
dkimRecord,
domain,
);
}
await load(); await load();
} }
} }

View File

@ -264,12 +264,22 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
final ServerHostingDetails serverDetails, final ServerHostingDetails serverDetails,
) async { ) async {
await repository.saveServerDetails(serverDetails); await repository.saveServerDetails(serverDetails);
/// TODO: Error handling?
await ProvidersController.currentDnsProvider!.removeDomainRecords( await ProvidersController.currentDnsProvider!.removeDomainRecords(
ip4: serverDetails.ip4, records: getProjectDnsRecords(
state.serverDomain!.domainName,
serverDetails.ip4,
false,
),
domain: state.serverDomain!, domain: state.serverDomain!,
); );
await ProvidersController.currentDnsProvider!.createDomainRecords( await ProvidersController.currentDnsProvider!.createDomainRecords(
ip4: serverDetails.ip4, records: getProjectDnsRecords(
state.serverDomain!.domainName,
serverDetails.ip4,
true,
),
domain: state.serverDomain!, domain: state.serverDomain!,
); );

View File

@ -558,14 +558,30 @@ class ServerInstallationRepository {
} }
Future<bool> deleteServer(final ServerDomain serverDomain) async { Future<bool> deleteServer(final ServerDomain serverDomain) async {
final ServerApi api = ServerApi();
final dnsRecords = await api.getDnsRecords();
final GenericResult<void> removalResult =
await ProvidersController.currentDnsProvider!.removeDomainRecords(
domain: serverDomain,
records: dnsRecords,
);
if (!removalResult.success) {
getIt<NavigationService>().showSnackBar(
'modals.dns_removal_error'.tr(),
);
return false;
}
final deletionResult = final deletionResult =
await ProvidersController.currentServerProvider!.deleteServer( await ProvidersController.currentServerProvider!.deleteServer(
serverDomain.domainName, serverDomain.domainName,
); );
if (!deletionResult.success) { if (!deletionResult.success) {
getIt<NavigationService>() getIt<NavigationService>().showSnackBar(
.showSnackBar('modals.server_validators_error'.tr()); 'modals.server_validators_error'.tr(),
);
return false; return false;
} }
@ -576,13 +592,6 @@ class ServerInstallationRepository {
await box.put(BNames.isLoading, false); await box.put(BNames.isLoading, false);
await box.put(BNames.serverDetails, null); await box.put(BNames.serverDetails, null);
final GenericResult<void> removalResult = await ProvidersController
.currentDnsProvider!
.removeDomainRecords(domain: serverDomain);
if (!removalResult.success) {
getIt<NavigationService>().showSnackBar('modals.dns_removal_error'.tr());
}
return true; return true;
} }

View File

@ -4,14 +4,18 @@ CloudflareDnsRecord _fromDnsRecord(
final DnsRecord dnsRecord, final DnsRecord dnsRecord,
final String rootDomain, final String rootDomain,
) { ) {
final String type = dnsRecord.type;
String name = dnsRecord.name ?? ''; String name = dnsRecord.name ?? '';
if (name != rootDomain && name != '@') { if (name != rootDomain && name != '@') {
name = '$name.$rootDomain'; name = '$name.$rootDomain';
} }
if (type == 'MX' && name == '@') {
name = rootDomain;
}
return CloudflareDnsRecord( return CloudflareDnsRecord(
content: dnsRecord.content, content: dnsRecord.content,
name: name, name: name,
type: dnsRecord.type, type: type,
zoneName: rootDomain, zoneName: rootDomain,
id: null, id: null,
ttl: dnsRecord.ttl, ttl: dnsRecord.ttl,

View File

@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/cloudflare_dns_info.dart'; import 'package:selfprivacy/logic/models/json/dns_providers/cloudflare_dns_info.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ApiAdapter { class ApiAdapter {
ApiAdapter({ ApiAdapter({
@ -80,15 +79,14 @@ class CloudflareDnsProvider extends DnsProvider {
@override @override
Future<GenericResult<void>> createDomainRecords({ Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}) async { }) async {
final syncZoneIdResult = await syncZoneId(domain.domainName); final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) { if (!syncZoneIdResult.success) {
return syncZoneIdResult; return syncZoneIdResult;
} }
final records = getProjectDnsRecords(domain.domainName, ip4);
return _adapter.api().createMultipleDnsRecords( return _adapter.api().createMultipleDnsRecords(
zoneId: _adapter.cachedZoneId, zoneId: _adapter.cachedZoneId,
records: records records: records
@ -102,16 +100,17 @@ class CloudflareDnsProvider extends DnsProvider {
@override @override
Future<GenericResult<void>> removeDomainRecords({ Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}) async { }) async {
final syncZoneIdResult = await syncZoneId(domain.domainName); final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) { if (!syncZoneIdResult.success) {
return syncZoneIdResult; return syncZoneIdResult;
} }
final result = final result = await _adapter.api().getDnsRecords(
await _adapter.api().getDnsRecords(zoneId: _adapter.cachedZoneId); zoneId: _adapter.cachedZoneId,
);
if (result.data.isEmpty || !result.success) { if (result.data.isEmpty || !result.success) {
return GenericResult( return GenericResult(
success: result.success, success: result.success,
@ -121,9 +120,29 @@ class CloudflareDnsProvider extends DnsProvider {
); );
} }
final List<CloudflareDnsRecord> selfprivacyRecords = records
.map(
(final record) => CloudflareDnsRecord.fromDnsRecord(
record,
domain.domainName,
),
)
.toList();
final List<CloudflareDnsRecord> cloudflareRecords = result.data;
/// Remove all records that do not match with SelfPrivacy
cloudflareRecords.removeWhere(
(final cloudflareRecord) => !selfprivacyRecords.any(
(final selfprivacyRecord) =>
selfprivacyRecord.type == cloudflareRecord.type &&
selfprivacyRecord.name == cloudflareRecord.name,
),
);
return _adapter.api().removeSimilarRecords( return _adapter.api().removeSimilarRecords(
zoneId: _adapter.cachedZoneId, zoneId: _adapter.cachedZoneId,
records: result.data, records: cloudflareRecords,
); );
} }

View File

@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/desec_dns_info.dart'; import 'package:selfprivacy/logic/models/json/dns_providers/desec_dns_info.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ApiAdapter { class ApiAdapter {
ApiAdapter({final bool isWithToken = true}) ApiAdapter({final bool isWithToken = true})
@ -75,16 +74,11 @@ class DesecDnsProvider extends DnsProvider {
@override @override
Future<GenericResult<void>> createDomainRecords({ Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}) async { }) async {
final List<DnsRecord> listDnsRecords = getProjectDnsRecords(
domain.domainName,
ip4,
);
final List<DesecDnsRecord> bulkRecords = []; final List<DesecDnsRecord> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) { for (final DnsRecord record in records) {
bulkRecords.add(DesecDnsRecord.fromDnsRecord(record, domain.domainName)); bulkRecords.add(DesecDnsRecord.fromDnsRecord(record, domain.domainName));
} }
@ -96,21 +90,19 @@ class DesecDnsProvider extends DnsProvider {
@override @override
Future<GenericResult<void>> removeDomainRecords({ Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}) async { }) async {
final List<DnsRecord> listDnsRecords = getProjectDnsRecords(
domain.domainName,
ip4,
);
final List<DesecDnsRecord> bulkRecords = []; final List<DesecDnsRecord> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) { for (final DnsRecord record in records) {
final desecRecord = DesecDnsRecord.fromDnsRecord( final desecRecord = DesecDnsRecord.fromDnsRecord(
record, record,
domain.domainName, domain.domainName,
); );
bulkRecords.add( bulkRecords.add(
/// Yes, it looks weird, but exactly forcing 'records' field
/// to empty array signals deSEC to remove the DNS record completely
/// https://desec.readthedocs.io/en/latest/dns/rrsets.html#deleting-an-rrset
DesecDnsRecord( DesecDnsRecord(
subname: desecRecord.subname, subname: desecRecord.subname,
type: desecRecord.type, type: desecRecord.type,
@ -119,14 +111,6 @@ class DesecDnsProvider extends DnsProvider {
), ),
); );
} }
bulkRecords.add(
DesecDnsRecord(
subname: 'selector._domainkey',
type: 'TXT',
ttl: 18000,
records: [],
),
);
return _adapter.api().removeSimilarRecords( return _adapter.api().removeSimilarRecords(
domainName: domain.domainName, domainName: domain.domainName,

View File

@ -3,7 +3,6 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/digital_ocean_dns_info.dart'; import 'package:selfprivacy/logic/models/json/dns_providers/digital_ocean_dns_info.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ApiAdapter { class ApiAdapter {
ApiAdapter({final bool isWithToken = true}) ApiAdapter({final bool isWithToken = true})
@ -75,15 +74,12 @@ class DigitalOceanDnsProvider extends DnsProvider {
@override @override
Future<GenericResult<void>> createDomainRecords({ Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}) async => }) async =>
_adapter.api().createMultipleDnsRecords( _adapter.api().createMultipleDnsRecords(
domainName: domain.domainName, domainName: domain.domainName,
records: getProjectDnsRecords( records: records
domain.domainName,
ip4,
)
.map<DigitalOceanDnsRecord>( .map<DigitalOceanDnsRecord>(
(final e) => (final e) =>
DigitalOceanDnsRecord.fromDnsRecord(e, domain.domainName), DigitalOceanDnsRecord.fromDnsRecord(e, domain.domainName),
@ -93,8 +89,8 @@ class DigitalOceanDnsProvider extends DnsProvider {
@override @override
Future<GenericResult<void>> removeDomainRecords({ Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}) async { }) async {
final result = await _adapter.api().getDnsRecords(domain.domainName); final result = await _adapter.api().getDnsRecords(domain.domainName);
if (result.data.isEmpty || !result.success) { if (result.data.isEmpty || !result.success) {
@ -106,17 +102,29 @@ class DigitalOceanDnsProvider extends DnsProvider {
); );
} }
const ignoreType = 'SOA'; final List<DigitalOceanDnsRecord> selfprivacyRecords = records
final List<DigitalOceanDnsRecord> filteredRecords = []; .map(
for (final record in result.data) { (final record) => DigitalOceanDnsRecord.fromDnsRecord(
if (record.type != ignoreType) { record,
filteredRecords.add(record); domain.domainName,
} ),
} )
.toList();
final List<DigitalOceanDnsRecord> oceanRecords = result.data;
/// Remove all records that do not match with SelfPrivacy
oceanRecords.removeWhere(
(final oceanRecord) => !selfprivacyRecords.any(
(final selfprivacyRecord) =>
selfprivacyRecord.type == oceanRecord.type &&
selfprivacyRecord.name == oceanRecord.name,
),
);
return _adapter.api().removeSimilarRecords( return _adapter.api().removeSimilarRecords(
domainName: domain.domainName, domainName: domain.domainName,
records: filteredRecords, records: oceanRecords,
); );
} }

View File

@ -23,23 +23,21 @@ abstract class DnsProvider {
/// Returns list of all available domain entries assigned to the account. /// Returns list of all available domain entries assigned to the account.
Future<GenericResult<List<ServerDomain>>> domainList(); Future<GenericResult<List<ServerDomain>>> domainList();
/// Tries to create all main domain records needed /// Tries to create domain records
/// for SelfPrivacy to launch on requested domain by ip4. /// by our records list.
/// ///
/// Doesn't check for duplication, cleaning has /// Doesn't check for duplication, cleaning has
/// to be done beforehand by [removeDomainRecords] /// to be done beforehand by [removeDomainRecords]
Future<GenericResult<void>> createDomainRecords({ Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}); });
/// Tries to remove all domain records of requested domain by ip4. /// Tries to remove all records of requested
/// /// domain that match our records list.
/// Will remove all entries, including the ones
/// that weren't created by SelfPrivacy.
Future<GenericResult<void>> removeDomainRecords({ Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain, required final ServerDomain domain,
final String? ip4,
}); });
/// Returns list of all [DnsRecord] entries assigned to requested domain. /// Returns list of all [DnsRecord] entries assigned to requested domain.

View File

@ -93,6 +93,7 @@ void launchURL(final url) async {
List<DnsRecord> getProjectDnsRecords( List<DnsRecord> getProjectDnsRecords(
final String? domainName, final String? domainName,
final String? ip4, final String? ip4,
final bool isCreating,
) { ) {
final DnsRecord domainA = final DnsRecord domainA =
DnsRecord(type: 'A', name: domainName, content: ip4); DnsRecord(type: 'A', name: domainName, content: ip4);
@ -121,6 +122,16 @@ List<DnsRecord> getProjectDnsRecords(
ttl: 18000, ttl: 18000,
); );
/// We never create this record!
/// This declaration is only for removal
/// as we need to compare by 'type' and 'name'
final DnsRecord txt3 = DnsRecord(
type: 'TXT',
name: 'selector._domainkey',
content: 'v=DKIM1; k=rsa; p=none',
ttl: 18000,
);
return <DnsRecord>[ return <DnsRecord>[
domainA, domainA,
apiA, apiA,
@ -132,6 +143,7 @@ List<DnsRecord> getProjectDnsRecords(
mx, mx,
txt1, txt1,
txt2, txt2,
if (!isCreating) txt3,
vpn, vpn,
]; ];
} }