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 {
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
final List<DnsRecord> records = await api.getDnsRecords();
/// TODO: Error handling?
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4;
await ProvidersController.currentDnsProvider!.removeDomainRecords(
records: records,
domain: domain!,
);
await ProvidersController.currentDnsProvider!.createDomainRecords(
records: records,
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();
}
}

View File

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

View File

@ -558,14 +558,30 @@ class ServerInstallationRepository {
}
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 =
await ProvidersController.currentServerProvider!.deleteServer(
serverDomain.domainName,
);
if (!deletionResult.success) {
getIt<NavigationService>()
.showSnackBar('modals.server_validators_error'.tr());
getIt<NavigationService>().showSnackBar(
'modals.server_validators_error'.tr(),
);
return false;
}
@ -576,13 +592,6 @@ class ServerInstallationRepository {
await box.put(BNames.isLoading, false);
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;
}

View File

@ -4,14 +4,18 @@ CloudflareDnsRecord _fromDnsRecord(
final DnsRecord dnsRecord,
final String rootDomain,
) {
final String type = dnsRecord.type;
String name = dnsRecord.name ?? '';
if (name != rootDomain && name != '@') {
name = '$name.$rootDomain';
}
if (type == 'MX' && name == '@') {
name = rootDomain;
}
return CloudflareDnsRecord(
content: dnsRecord.content,
name: name,
type: dnsRecord.type,
type: type,
zoneName: rootDomain,
id: null,
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_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ApiAdapter {
ApiAdapter({
@ -80,15 +79,14 @@ class CloudflareDnsProvider extends DnsProvider {
@override
Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
}) async {
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return syncZoneIdResult;
}
final records = getProjectDnsRecords(domain.domainName, ip4);
return _adapter.api().createMultipleDnsRecords(
zoneId: _adapter.cachedZoneId,
records: records
@ -102,16 +100,17 @@ class CloudflareDnsProvider extends DnsProvider {
@override
Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
}) async {
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return syncZoneIdResult;
}
final result =
await _adapter.api().getDnsRecords(zoneId: _adapter.cachedZoneId);
final result = await _adapter.api().getDnsRecords(
zoneId: _adapter.cachedZoneId,
);
if (result.data.isEmpty || !result.success) {
return GenericResult(
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(
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_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
@ -75,16 +74,11 @@ class DesecDnsProvider extends DnsProvider {
@override
Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
}) async {
final List<DnsRecord> listDnsRecords = getProjectDnsRecords(
domain.domainName,
ip4,
);
final List<DesecDnsRecord> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) {
for (final DnsRecord record in records) {
bulkRecords.add(DesecDnsRecord.fromDnsRecord(record, domain.domainName));
}
@ -96,21 +90,19 @@ class DesecDnsProvider extends DnsProvider {
@override
Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
}) async {
final List<DnsRecord> listDnsRecords = getProjectDnsRecords(
domain.domainName,
ip4,
);
final List<DesecDnsRecord> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) {
for (final DnsRecord record in records) {
final desecRecord = DesecDnsRecord.fromDnsRecord(
record,
domain.domainName,
);
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(
subname: desecRecord.subname,
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(
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_records.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class ApiAdapter {
ApiAdapter({final bool isWithToken = true})
@ -75,15 +74,12 @@ class DigitalOceanDnsProvider extends DnsProvider {
@override
Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
}) async =>
_adapter.api().createMultipleDnsRecords(
domainName: domain.domainName,
records: getProjectDnsRecords(
domain.domainName,
ip4,
)
records: records
.map<DigitalOceanDnsRecord>(
(final e) =>
DigitalOceanDnsRecord.fromDnsRecord(e, domain.domainName),
@ -93,8 +89,8 @@ class DigitalOceanDnsProvider extends DnsProvider {
@override
Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
}) async {
final result = await _adapter.api().getDnsRecords(domain.domainName);
if (result.data.isEmpty || !result.success) {
@ -106,17 +102,29 @@ class DigitalOceanDnsProvider extends DnsProvider {
);
}
const ignoreType = 'SOA';
final List<DigitalOceanDnsRecord> filteredRecords = [];
for (final record in result.data) {
if (record.type != ignoreType) {
filteredRecords.add(record);
}
}
final List<DigitalOceanDnsRecord> selfprivacyRecords = records
.map(
(final record) => DigitalOceanDnsRecord.fromDnsRecord(
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(
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.
Future<GenericResult<List<ServerDomain>>> domainList();
/// Tries to create all main domain records needed
/// for SelfPrivacy to launch on requested domain by ip4.
/// Tries to create domain records
/// by our records list.
///
/// Doesn't check for duplication, cleaning has
/// to be done beforehand by [removeDomainRecords]
Future<GenericResult<void>> createDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
});
/// Tries to remove all domain records of requested domain by ip4.
///
/// Will remove all entries, including the ones
/// that weren't created by SelfPrivacy.
/// Tries to remove all records of requested
/// domain that match our records list.
Future<GenericResult<void>> removeDomainRecords({
required final List<DnsRecord> records,
required final ServerDomain domain,
final String? ip4,
});
/// Returns list of all [DnsRecord] entries assigned to requested domain.

View File

@ -93,6 +93,7 @@ void launchURL(final url) async {
List<DnsRecord> getProjectDnsRecords(
final String? domainName,
final String? ip4,
final bool isCreating,
) {
final DnsRecord domainA =
DnsRecord(type: 'A', name: domainName, content: ip4);
@ -121,6 +122,16 @@ List<DnsRecord> getProjectDnsRecords(
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>[
domainA,
apiA,
@ -132,6 +143,7 @@ List<DnsRecord> getProjectDnsRecords(
mx,
txt1,
txt2,
if (!isCreating) txt3,
vpn,
];
}