feat: Make DNS records deletion and creation dynamic #424

Merged
NaiJi merged 4 commits from dynamic-dns-deletion into master 2024-01-19 15:46:58 +02:00
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(
inex marked this conversation as resolved
Review

We really shouldn't create empty DKIM records.

We really shouldn't create empty DKIM records.

We don't. Look at 143 line

We don't. Look at 143 line
Review

Ah, so is it used only for deletion? Probably worth putting a comment about it.

Ah, so is it used only for deletion? Probably worth putting a comment about it.
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,
]; ];
} }