diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart index 1584e819..2226cdd1 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart @@ -155,7 +155,7 @@ class DesecApi extends RestApiMap { return GenericResult(success: true, data: null); } - Future> removeSimilarRecords({ + Future> putRecords({ required final String domainName, required final List records, }) async { diff --git a/lib/logic/models/json/dns_providers/cloudflare_dns_info.dart b/lib/logic/models/json/dns_providers/cloudflare_dns_info.dart index c2ee727b..ec3c837c 100644 --- a/lib/logic/models/json/dns_providers/cloudflare_dns_info.dart +++ b/lib/logic/models/json/dns_providers/cloudflare_dns_info.dart @@ -43,6 +43,7 @@ class CloudflareDnsRecord { this.ttl = 3600, this.priority = 10, this.id, + this.comment = 'Created by SelfPrivacy app', }); factory CloudflareDnsRecord.fromDnsRecord( @@ -90,6 +91,9 @@ class CloudflareDnsRecord { /// `>= 0 <= 65535` final int priority; + /// Comments or notes about the DNS record. This field has no effect on DNS responses. + final String comment; + static CloudflareDnsRecord fromJson(final Map json) => _$CloudflareDnsRecordFromJson(json); Map toJson() => _$CloudflareDnsRecordToJson(this); diff --git a/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart b/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart index 03b871dc..298fc205 100644 --- a/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart +++ b/lib/logic/models/json/dns_providers/cloudflare_dns_info.g.dart @@ -27,6 +27,7 @@ CloudflareDnsRecord _$CloudflareDnsRecordFromJson(Map json) => ttl: (json['ttl'] as num?)?.toInt() ?? 3600, priority: (json['priority'] as num?)?.toInt() ?? 10, id: json['id'] as String?, + comment: json['comment'] as String? ?? 'Created by SelfPrivacy app', ); Map _$CloudflareDnsRecordToJson( @@ -39,4 +40,5 @@ Map _$CloudflareDnsRecordToJson( 'zone_name': instance.zoneName, 'ttl': instance.ttl, 'priority': instance.priority, + 'comment': instance.comment, }; diff --git a/lib/logic/models/json/dns_records.dart b/lib/logic/models/json/dns_records.dart index 28eb71cb..f0ae9b4d 100644 --- a/lib/logic/models/json/dns_records.dart +++ b/lib/logic/models/json/dns_records.dart @@ -1,11 +1,12 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart'; part 'dns_records.g.dart'; -@JsonSerializable(createToJson: true, createFactory: false) -class DnsRecord { - DnsRecord({ +@JsonSerializable() +class DnsRecord extends Equatable { + const DnsRecord({ required this.type, required this.name, required this.content, @@ -35,4 +36,14 @@ class DnsRecord { final bool proxied; Map toJson() => _$DnsRecordToJson(this); + + @override + @JsonKey(includeToJson: false) + List get props => [ + type, + name, + content, + ttl, + priority, + ]; } diff --git a/lib/logic/models/json/dns_records.g.dart b/lib/logic/models/json/dns_records.g.dart index ab4603cb..1ac0c4bd 100644 --- a/lib/logic/models/json/dns_records.g.dart +++ b/lib/logic/models/json/dns_records.g.dart @@ -6,6 +6,16 @@ part of 'dns_records.dart'; // JsonSerializableGenerator // ************************************************************************** +DnsRecord _$DnsRecordFromJson(Map json) => DnsRecord( + type: json['type'] as String, + name: json['name'] as String?, + content: json['content'] as String?, + displayName: json['displayName'] as String?, + ttl: (json['ttl'] as num?)?.toInt() ?? 3600, + priority: (json['priority'] as num?)?.toInt() ?? 10, + proxied: json['proxied'] as bool? ?? false, + ); + Map _$DnsRecordToJson(DnsRecord instance) => { 'type': instance.type, 'displayName': instance.displayName, diff --git a/lib/logic/providers/dns_providers/cloudflare.dart b/lib/logic/providers/dns_providers/cloudflare.dart index eb99db1d..156083a2 100644 --- a/lib/logic/providers/dns_providers/cloudflare.dart +++ b/lib/logic/providers/dns_providers/cloudflare.dart @@ -198,6 +198,94 @@ class CloudflareDnsProvider extends DnsProvider { ); } + @override + Future> updateDnsRecords({ + required final List newRecords, + required final ServerDomain domain, + final List? oldRecords, + }) async { + final syncZoneIdResult = await syncZoneId(domain.domainName); + if (!syncZoneIdResult.success) { + return syncZoneIdResult; + } + + final result = await _adapter.api().getDnsRecords( + zoneId: _adapter.cachedZoneId, + ); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: false, + data: null, + code: result.code, + message: result.message, + ); + } + + final List newSelfprivacyRecords = newRecords + .map( + (final record) => CloudflareDnsRecord.fromDnsRecord( + record, + domain.domainName, + ), + ) + .toList(); + + final List? oldSelfprivacyRecords = oldRecords + ?.map( + (final record) => CloudflareDnsRecord.fromDnsRecord( + record, + domain.domainName, + ), + ) + .toList(); + + final List cloudflareRecords = result.data; + + final List recordsToDelete = newSelfprivacyRecords + .where( + (final newRecord) => cloudflareRecords.any( + (final oldRecord) => + newRecord.type == oldRecord.type && + newRecord.name == oldRecord.name, + ), + ) + .toList(); + + if (oldSelfprivacyRecords != null) { + recordsToDelete.addAll( + oldSelfprivacyRecords + .where( + (final oldRecord) => !newSelfprivacyRecords.any( + (final newRecord) => + newRecord.type == oldRecord.type && + newRecord.name == oldRecord.name, + ), + ) + .toList(), + ); + } + + if (recordsToDelete.isNotEmpty) { + await _adapter.api().removeSimilarRecords( + records: cloudflareRecords + .where( + (final record) => recordsToDelete.any( + (final recordToDelete) => + recordToDelete.type == record.type && + recordToDelete.name == record.name, + ), + ) + .toList(), + zoneId: _adapter.cachedZoneId, + ); + } + + return _adapter.api().createMultipleDnsRecords( + zoneId: _adapter.cachedZoneId, + records: newSelfprivacyRecords, + ); + } + Future> syncZoneId(final String domain) async { if (domain == _adapter.cachedDomain && _adapter.cachedZoneId.isNotEmpty) { return GenericResult( diff --git a/lib/logic/providers/dns_providers/desec.dart b/lib/logic/providers/dns_providers/desec.dart index eaab3aaf..eb0f2b9a 100644 --- a/lib/logic/providers/dns_providers/desec.dart +++ b/lib/logic/providers/dns_providers/desec.dart @@ -112,7 +112,28 @@ class DesecDnsProvider extends DnsProvider { ); } - return _adapter.api().removeSimilarRecords( + return _adapter.api().putRecords( + domainName: domain.domainName, + records: bulkRecords, + ); + } + + @override + Future> updateDnsRecords({ + required final List newRecords, + required final ServerDomain domain, + final List? oldRecords, + }) async { + if (oldRecords != null) { + await removeDomainRecords(records: oldRecords, domain: domain); + } + + final List bulkRecords = []; + for (final DnsRecord record in newRecords) { + bulkRecords.add(DesecDnsRecord.fromDnsRecord(record, domain.domainName)); + } + + return _adapter.api().putRecords( domainName: domain.domainName, records: bulkRecords, ); diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart index 093123b8..bf35b77b 100644 --- a/lib/logic/providers/dns_providers/digital_ocean_dns.dart +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -128,6 +128,79 @@ class DigitalOceanDnsProvider extends DnsProvider { ); } + @override + Future> updateDnsRecords({ + required final List newRecords, + required final ServerDomain domain, + final List? oldRecords, + }) async { + final result = await _adapter.api().getDnsRecords(domain.domainName); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: null, + code: result.code, + message: result.message, + ); + } + + final List newSelfprivacyRecords = newRecords + .map( + (final record) => DigitalOceanDnsRecord.fromDnsRecord( + record, + domain.domainName, + ), + ) + .toList(); + + final List? oldSelfprivacyRecords = oldRecords + ?.map( + (final record) => DigitalOceanDnsRecord.fromDnsRecord( + record, + domain.domainName, + ), + ) + .toList(); + + final List oceanRecords = result.data; + + final List recordsToDelete = newSelfprivacyRecords + .where( + (final newRecord) => oceanRecords.any( + (final oldRecord) => + newRecord.type == oldRecord.type && + newRecord.name == oldRecord.name, + ), + ) + .toList(); + + if (oldSelfprivacyRecords != null) { + recordsToDelete.addAll( + oldSelfprivacyRecords + .where( + (final oldRecord) => !newSelfprivacyRecords.any( + (final newRecord) => + newRecord.type == oldRecord.type && + newRecord.name == oldRecord.name, + ), + ) + .toList(), + ); + } + + if (recordsToDelete.isNotEmpty) { + return _adapter.api().removeSimilarRecords( + domainName: domain.domainName, + records: recordsToDelete, + ); + } + + return _adapter.api().createMultipleDnsRecords( + domainName: domain.domainName, + records: newSelfprivacyRecords, + ); + } + @override Future>> getDnsRecords({ required final ServerDomain domain, diff --git a/lib/logic/providers/dns_providers/dns_provider.dart b/lib/logic/providers/dns_providers/dns_provider.dart index 406368b0..2066ebcf 100644 --- a/lib/logic/providers/dns_providers/dns_provider.dart +++ b/lib/logic/providers/dns_providers/dns_provider.dart @@ -54,4 +54,14 @@ abstract class DnsProvider { final DnsRecord record, final ServerDomain domain, ); + + /// Tries to update existing domain records + /// + /// If [oldRecords] is provided, it will also remove the records that + /// are not in [newRecords + Future> updateDnsRecords({ + required final List newRecords, + required final ServerDomain domain, + final List? oldRecords, + }); } diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index c155d6a0..bbd21628 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -108,7 +108,7 @@ List getProjectDnsRecords( final DnsRecord socialA = DnsRecord(type: 'A', name: 'social', content: ip4); final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); - final DnsRecord txt1 = DnsRecord( + const DnsRecord txt1 = DnsRecord( type: 'TXT', name: '_dmarc', content: 'v=DMARC1; p=none', @@ -125,7 +125,7 @@ List getProjectDnsRecords( /// We never create this record! /// This declaration is only for removal /// as we need to compare by 'type' and 'name' - final DnsRecord txt3 = DnsRecord( + const DnsRecord txt3 = DnsRecord( type: 'TXT', name: 'selector._domainkey', content: 'v=DKIM1; k=rsa; p=none',