From e9665ad75d1dcc2cd4768801a6f939eebcd53917 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 16 May 2023 11:06:01 -0300 Subject: [PATCH] feat: Implement polymorphic DNS check for DNS API --- .../dns_providers/cloudflare/cloudflare.dart | 144 +++++++++++ .../rest_maps/dns_providers/desec/desec.dart | 226 +++++++++++++++--- .../rest_maps/dns_providers/dns_provider.dart | 12 +- .../server_providers/hetzner/hetzner.dart | 2 +- .../cubit/dns_records/dns_records_cubit.dart | 92 +++---- lib/utils/network_utils.dart | 81 ------- 6 files changed, 385 insertions(+), 172 deletions(-) diff --git a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart index 04ff622b..eb22e4d8 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare.dart @@ -5,6 +5,7 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; class CloudflareApi extends DnsProviderApi { CloudflareApi({ @@ -317,4 +318,147 @@ class CloudflareApi extends DnsProviderApi { return domains; } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final List records = await getDnsRecords(domain: domain); + final List foundRecords = []; + try { + final List desiredRecords = + getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); + for (final DesiredDnsRecord record in desiredRecords) { + if (record.description == 'record.dkim') { + final DnsRecord foundRecord = records.firstWhere( + (final r) => (r.name == record.name) && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final String? foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.any( + (final r) => + (r.name == record.name) && + r.type == record.type && + r.content == record.content, + )) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + } catch (e) { + print(e); + return APIGenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return APIGenericResult( + data: foundRecords, + success: true, + ); + } + + @override + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ) { + if (domainName == null || ip4 == null) { + return []; + } + return [ + DesiredDnsRecord( + name: domainName, + content: ip4, + description: 'record.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ip4, + description: 'record.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ip4, + description: 'record.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ip4, + description: 'record.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ip4, + description: 'record.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ip4, + description: 'record.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ip4, + description: 'record.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ip4, + description: 'record.vpn', + ), + DesiredDnsRecord( + name: domainName, + content: domainName, + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: 'v=DMARC1; p=none', + description: 'record.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: domainName, + content: 'v=spf1 a mx ip4:$ip4 -all', + description: 'record.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + if (dkimPublicKey != null) + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: dkimPublicKey, + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart index 4e55bbfa..91eed57a 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart @@ -5,6 +5,7 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; class DesecApi extends DnsProviderApi { DesecApi({ @@ -61,6 +62,7 @@ class DesecApi extends DnsProviderApi { headers: {'Authorization': 'Token $token'}, ), ); + await Future.delayed(const Duration(seconds: 1)); } catch (e) { print(e); isValid = false; @@ -102,13 +104,29 @@ class DesecApi extends DnsProviderApi { }) async { final String domainName = domain.domainName; final String url = '/$domainName/rrsets/'; + final List listDnsRecords = projectDnsRecords(domainName, ip4); final Dio client = await getClient(); try { - final Response response = await client.get(url); - - final List records = response.data; - await client.put(url, data: records); + final List bulkRecords = []; + for (final DnsRecord record in listDnsRecords) { + bulkRecords.add( + record.name == null + ? { + 'type': record.type, + 'ttl': record.ttl, + 'records': [], + } + : { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [], + }, + ); + } + await client.put(url, data: bulkRecords); + await Future.delayed(const Duration(seconds: 1)); } catch (e) { print(e); return APIGenericResult( @@ -136,14 +154,18 @@ class DesecApi extends DnsProviderApi { final Dio client = await getClient(); try { response = await client.get(url); + await Future.delayed(const Duration(seconds: 1)); final List records = response.data; for (final record in records) { + final String? content = (record['records'] is List) + ? record['records'][0] + : record['records']; allRecords.add( DnsRecord( name: record['subname'], type: record['type'], - content: record['records'], + content: content, ttl: record['ttl'], ), ); @@ -164,30 +186,31 @@ class DesecApi extends DnsProviderApi { }) async { final String domainName = domain.domainName; final List listDnsRecords = projectDnsRecords(domainName, ip4); - final List allCreateFutures = []; final Dio client = await getClient(); try { + final List bulkRecords = []; for (final DnsRecord record in listDnsRecords) { - allCreateFutures.add( - client.post( - '/$domainName/rrsets/', - data: record.name == null - ? { - 'type': record.type, - 'ttl': record.ttl, - 'records': [record.content], - } - : { - 'subname': record.name, - 'type': record.type, - 'ttl': record.ttl, - 'records': [record.content], - }, - ), + bulkRecords.add( + record.name == null + ? { + 'type': record.type, + 'ttl': record.ttl, + 'records': [record.content], + } + : { + 'subname': record.name, + 'type': record.type, + 'ttl': record.ttl, + 'records': [record.content], + }, ); } - await Future.wait(allCreateFutures); + await client.post( + '/$domainName/rrsets/', + data: bulkRecords, + ); + await Future.delayed(const Duration(seconds: 1)); } on DioError catch (e) { print(e.message); rethrow; @@ -209,9 +232,10 @@ class DesecApi extends DnsProviderApi { final String? domainName, final String? ip4, ) { - final DnsRecord domainA = DnsRecord(type: 'A', name: null, content: ip4); + final DnsRecord domainA = DnsRecord(type: 'A', name: '', content: ip4); - final DnsRecord mx = DnsRecord(type: 'MX', name: null, content: domainName); + final DnsRecord mx = + DnsRecord(type: 'MX', name: '', content: '10 $domainName.'); final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); @@ -225,14 +249,14 @@ class DesecApi extends DnsProviderApi { final DnsRecord txt1 = DnsRecord( type: 'TXT', name: '_dmarc', - content: 'v=DMARC1; p=none', + content: '"v=DMARC1; p=none"', ttl: 18000, ); final DnsRecord txt2 = DnsRecord( type: 'TXT', - name: null, - content: 'v=spf1 a mx ip4:$ip4 -all', + name: '', + content: '"v=spf1 a mx ip4:$ip4 -all"', ttl: 18000, ); @@ -275,6 +299,7 @@ class DesecApi extends DnsProviderApi { 'records': [record.content], }, ); + await Future.delayed(const Duration(seconds: 1)); } catch (e) { print(e); } finally { @@ -291,6 +316,7 @@ class DesecApi extends DnsProviderApi { final Response response = await client.get( '', ); + await Future.delayed(const Duration(seconds: 1)); domains = response.data .map((final el) => el['name'] as String) .toList(); @@ -302,4 +328,148 @@ class DesecApi extends DnsProviderApi { return domains; } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final List records = await getDnsRecords(domain: domain); + final List foundRecords = []; + try { + final List desiredRecords = + getDesiredDnsRecords(domain.domainName, ip4, dkimPublicKey); + for (final DesiredDnsRecord record in desiredRecords) { + if (record.description == 'record.dkim') { + final DnsRecord foundRecord = records.firstWhere( + (final r) => (r.name == record.name) && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final String? foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final String content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.any( + (final r) => + ('${r.name}.${domain.domainName}' == record.name || + record.name == '') && + r.type == record.type && + r.content == record.content, + )) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + } catch (e) { + print(e); + return APIGenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return APIGenericResult( + data: foundRecords, + success: true, + ); + } + + @override + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ) { + if (domainName == null || ip4 == null) { + return []; + } + return [ + DesiredDnsRecord( + name: '', + content: ip4, + description: 'record.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ip4, + description: 'record.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ip4, + description: 'record.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ip4, + description: 'record.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ip4, + description: 'record.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ip4, + description: 'record.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ip4, + description: 'record.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ip4, + description: 'record.vpn', + ), + DesiredDnsRecord( + name: '', + content: '10 $domainName.', + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: '"v=DMARC1; p=none"', + description: 'record.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '', + content: '"v=spf1 a mx ip4:$ip4 -all"', + description: 'record.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + if (dkimPublicKey != null) + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: dkimPublicKey, + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart index 106d185c..b85f94d1 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/dns_provider.dart @@ -2,6 +2,7 @@ import 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/api_map.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart'; +import 'package:selfprivacy/utils/network_utils.dart'; export 'package:selfprivacy/logic/api_maps/api_generic_result.dart'; @@ -26,9 +27,18 @@ abstract class DnsProviderApi extends ApiMap { final DnsRecord record, final ServerDomain domain, ); + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ); + List getDesiredDnsRecords( + final String? domainName, + final String? ip4, + final String? dkimPublicKey, + ); Future getZoneId(final String domain); Future> domainList(); - Future> isApiTokenValid(final String token); RegExp getApiTokenValidation(); } diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart index c2228030..f9a2104e 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner.dart @@ -394,7 +394,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi { final String apiToken = StringGenerators.apiToken(); final String hostname = getHostnameFromDomain(domainName); - const String infectBranch = 'providers/hetzner'; + const String infectBranch = 'testing/desec'; final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false'; final String base64Password = base64.encode(utf8.encode(rootUser.password ?? 'PASS')); diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 3403dc68..de87910d 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -25,11 +25,13 @@ class DnsRecordsCubit emit( DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, - dnsRecords: getDesiredDnsRecords( - serverInstallationCubit.state.serverDomain?.domainName, - '', - '', - ), + dnsRecords: ApiController.currentDnsProviderApiFactory! + .getDnsProvider() + .getDesiredDnsRecords( + serverInstallationCubit.state.serverDomain?.domainName, + '', + '', + ), ), ); @@ -37,64 +39,32 @@ class DnsRecordsCubit final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; - if (domain != null && ipAddress != null) { - final List records = await ApiController - .currentDnsProviderApiFactory! - .getDnsProvider() - .getDnsRecords(domain: domain); - final String? dkimPublicKey = - extractDkimRecord(await api.getDnsRecords())?.content; - final List desiredRecords = - getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey); - final List foundRecords = []; - for (final DesiredDnsRecord record in desiredRecords) { - if (record.description == 'record.dkim') { - final DnsRecord foundRecord = records.firstWhere( - (final r) => r.name == record.name && r.type == record.type, - orElse: () => DnsRecord( - name: record.name, - type: record.type, - content: '', - ttl: 800, - proxied: false, - ), - ); - // remove all spaces and tabulators from - // the foundRecord.content and the record.content - // to compare them - final String? foundContent = - foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); - final String content = - record.content.replaceAll(RegExp(r'\s+'), ''); - if (foundContent == content) { - foundRecords.add(record.copyWith(isSatisfied: true)); - } else { - foundRecords.add(record.copyWith(isSatisfied: false)); - } - } else { - if (records.any( - (final r) => - r.name == record.name && - r.type == record.type && - r.content == record.content, - )) { - foundRecords.add(record.copyWith(isSatisfied: true)); - } else { - foundRecords.add(record.copyWith(isSatisfied: false)); - } - } - } - emit( - DnsRecordsState( - dnsRecords: foundRecords, - dnsState: foundRecords.any((final r) => r.isSatisfied == false) - ? DnsRecordsStatus.error - : DnsRecordsStatus.good, - ), - ); - } else { + if (domain == null && ipAddress == null) { emit(const DnsRecordsState()); + return; } + + final foundRecords = await ApiController.currentDnsProviderApiFactory! + .getDnsProvider() + .validateDnsRecords( + domain!, + ipAddress!, + extractDkimRecord(await api.getDnsRecords())?.content ?? '', + ); + + if (!foundRecords.success || foundRecords.data.isEmpty) { + emit(const DnsRecordsState()); + return; + } + + emit( + DnsRecordsState( + dnsRecords: foundRecords.data, + dnsState: foundRecords.data.any((final r) => r.isSatisfied == false) + ? DnsRecordsStatus.error + : DnsRecordsStatus.good, + ), + ); } } diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index fc06ecb8..3b7f9219 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -41,87 +41,6 @@ class DesiredDnsRecord { ); } -List getDesiredDnsRecords( - final String? domainName, - final String? ipAddress, - final String? dkimPublicKey, -) { - if (domainName == null || ipAddress == null) { - return []; - } - return [ - DesiredDnsRecord( - name: domainName, - content: ipAddress, - description: 'record.root', - ), - DesiredDnsRecord( - name: 'api.$domainName', - content: ipAddress, - description: 'record.api', - ), - DesiredDnsRecord( - name: 'cloud.$domainName', - content: ipAddress, - description: 'record.cloud', - ), - DesiredDnsRecord( - name: 'git.$domainName', - content: ipAddress, - description: 'record.git', - ), - DesiredDnsRecord( - name: 'meet.$domainName', - content: ipAddress, - description: 'record.meet', - ), - DesiredDnsRecord( - name: 'social.$domainName', - content: ipAddress, - description: 'record.social', - ), - DesiredDnsRecord( - name: 'password.$domainName', - content: ipAddress, - description: 'record.password', - ), - DesiredDnsRecord( - name: 'vpn.$domainName', - content: ipAddress, - description: 'record.vpn', - ), - DesiredDnsRecord( - name: domainName, - content: domainName, - description: 'record.mx', - type: 'MX', - category: DnsRecordsCategory.email, - ), - DesiredDnsRecord( - name: '_dmarc.$domainName', - content: 'v=DMARC1; p=none', - description: 'record.dmarc', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - DesiredDnsRecord( - name: domainName, - content: 'v=spf1 a mx ip4:$ipAddress -all', - description: 'record.spf', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - if (dkimPublicKey != null) - DesiredDnsRecord( - name: 'selector._domainkey.$domainName', - content: dkimPublicKey, - description: 'record.dkim', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - ]; -} - DnsRecord? extractDkimRecord(final List records) { DnsRecord? dkimRecord;