From b0769b8ed0ff8b4d67952ccb978b767db429053a Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 30 May 2023 00:04:29 -0300 Subject: [PATCH] chore: Separate business logic from API layer for Digital Ocean DNS --- ...an_dns.dart => digital_ocean_dns_api.dart} | 191 ++-------- ...art => digital_ocean_dns_api_factory.dart} | 2 +- .../dns_providers/digital_ocean.dart | 3 - .../dns_providers/digital_ocean_dns.dart | 356 ++++++++++++++++++ .../dns_providers/dns_provider_factory.dart | 2 +- 5 files changed, 384 insertions(+), 170 deletions(-) rename lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/{digital_ocean_dns.dart => digital_ocean_dns_api.dart} (51%) rename lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/{digital_ocean_dns_factory.dart => digital_ocean_dns_api_factory.dart} (93%) delete mode 100644 lib/logic/providers/dns_providers/digital_ocean.dart create mode 100644 lib/logic/providers/dns_providers/digital_ocean_dns.dart diff --git a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart similarity index 51% rename from lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart rename to lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart index 9a33bdfb..b0e94f41 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart @@ -92,28 +92,19 @@ class DigitalOceanDnsApi extends DnsProviderApi { ); } - @override - // TODO: Remove from DnsProviderInterface, stub for now - Future getZoneId(final String domain) async => domain; - - @override Future> removeSimilarRecords({ required final ServerDomain domain, - final String? ip4, + required final List records, }) async { final String domainName = domain.domainName; final Dio client = await getClient(); try { - const String ignoreType = 'SOA'; final List allDeleteFutures = []; - final List records = await getDnsRecords(domain: domain); for (final record in records) { - if (record.type != ignoreType) { - allDeleteFutures.add( - client.delete('/domains/$domainName/records/${record.id}'), - ); - } + allDeleteFutures.add( + client.delete('/domains/$domainName/records/${record.id}'), + ); } await Future.wait(allDeleteFutures); } catch (e) { @@ -130,13 +121,12 @@ class DigitalOceanDnsApi extends DnsProviderApi { return GenericResult(success: true, data: null); } - @override - Future> getDnsRecords({ + Future> getDnsRecords({ required final ServerDomain domain, }) async { Response response; final String domainName = domain.domainName; - final List allRecords = []; + List allRecords = []; /// Default amount is 20, but we will eventually overflow it, /// so I hardcode it to the maximum available amount in advance just in case @@ -148,144 +138,38 @@ class DigitalOceanDnsApi extends DnsProviderApi { final Dio client = await getClient(); try { response = await client.get(url); - final List records = response.data['domain_records'] ?? []; - - for (final record in records) { - allRecords.add( - DnsRecord( - id: record['id'], - name: record['name'], - type: record['type'], - content: record['data'], - ttl: record['ttl'], - proxied: false, - ), - ); - } + allRecords = response.data['domain_records'] ?? []; } catch (e) { print(e); + GenericResult( + data: allRecords, + success: false, + message: e.toString(), + ); } finally { close(client); } - return allRecords; + return GenericResult(data: allRecords, success: true); } - Future>> validateDnsRecords( - final ServerDomain domain, - final String ip4, - final String dkimPublicKey, - ); - - @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', - displayName: domainName, - ), - DesiredDnsRecord( - name: 'api', - content: ip4, - description: 'record.api', - displayName: 'api.$domainName', - ), - DesiredDnsRecord( - name: 'cloud', - content: ip4, - description: 'record.cloud', - displayName: 'cloud.$domainName', - ), - DesiredDnsRecord( - name: 'git', - content: ip4, - description: 'record.git', - displayName: 'git.$domainName', - ), - DesiredDnsRecord( - name: 'meet', - content: ip4, - description: 'record.meet', - displayName: 'meet.$domainName', - ), - DesiredDnsRecord( - name: 'social', - content: ip4, - description: 'record.social', - displayName: 'social.$domainName', - ), - DesiredDnsRecord( - name: 'password', - content: ip4, - description: 'record.password', - displayName: 'password.$domainName', - ), - DesiredDnsRecord( - name: 'vpn', - content: ip4, - description: 'record.vpn', - displayName: 'vpn.$domainName', - ), - const DesiredDnsRecord( - name: '@', - content: '@', - description: 'record.mx', - type: 'MX', - category: DnsRecordsCategory.email, - ), - const DesiredDnsRecord( - name: '_dmarc', - 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', - content: dkimPublicKey, - description: 'record.dkim', - type: 'TXT', - category: DnsRecordsCategory.email, - ), - ]; - } - - @override Future> createMultipleDnsRecords({ required final ServerDomain domain, - final String? ip4, + required final List records, }) async { final String domainName = domain.domainName; - final List dnsRecords = getProjectDnsRecords(domainName, ip4); final List allCreateFutures = []; final Dio client = await getClient(); try { - for (final DnsRecord record in dnsRecords) { + for (final DnsRecord record in records) { allCreateFutures.add( client.post( '/domains/$domainName/records', data: { 'type': record.type, - 'name': record.name == domainName ? '@' : record.name, - 'data': record.type == 'MX' ? '@' : record.content, + 'name': record.name, + 'data': record.content, 'ttl': record.ttl, 'priority': record.priority, }, @@ -310,47 +194,24 @@ class DigitalOceanDnsApi extends DnsProviderApi { return GenericResult(success: true, data: null); } - @override - Future setDnsRecord( - final DnsRecord record, - final ServerDomain domain, - ) async { - final Dio client = await getClient(); - try { - final domainName = domain.domainName; - await client.post( - '/domains/$domainName/records', - data: { - 'type': record.type, - 'name': record.name, - 'data': record.content, - 'ttl': record.ttl, - 'priority': record.priority, - }, - ); - } catch (e) { - print(e); - } finally { - close(client); - } - } - - @override - Future> domainList() async { - List domains = []; + Future> domainList() async { + List domains = []; final Dio client = await getClient(); try { final Response response = await client.get('/domains'); - domains = response.data['domains'] - .map((final el) => el['name'] as String) - .toList(); + domains = response.data['domains']; } catch (e) { print(e); + return GenericResult( + data: domains, + success: false, + message: e.toString(), + ); } finally { close(client); } - return domains; + return GenericResult(data: domains, success: true); } } diff --git a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api_factory.dart similarity index 93% rename from lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_factory.dart rename to lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api_factory.dart index 715a4178..4be2c74b 100644 --- a/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_factory.dart +++ b/lib/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api_factory.dart @@ -1,4 +1,4 @@ -import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; diff --git a/lib/logic/providers/dns_providers/digital_ocean.dart b/lib/logic/providers/dns_providers/digital_ocean.dart deleted file mode 100644 index 2a8df75b..00000000 --- a/lib/logic/providers/dns_providers/digital_ocean.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; - -class DigitalOceanDnsProvider extends DnsProvider {} diff --git a/lib/logic/providers/dns_providers/digital_ocean_dns.dart b/lib/logic/providers/dns_providers/digital_ocean_dns.dart new file mode 100644 index 00000000..ca34ad16 --- /dev/null +++ b/lib/logic/providers/dns_providers/digital_ocean_dns.dart @@ -0,0 +1,356 @@ +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.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/logic/providers/dns_providers/dns_provider.dart'; + +class ApiAdapter { + ApiAdapter({final bool isWithToken = true}) + : _api = DigitalOceanDnsApi( + isWithToken: isWithToken, + ); + + DigitalOceanDnsApi api({final bool getInitialized = true}) => getInitialized + ? _api + : DigitalOceanDnsApi( + isWithToken: false, + ); + + final DigitalOceanDnsApi _api; +} + +class DigitalOceanDnsProvider extends DnsProvider { + DigitalOceanDnsProvider() : _adapter = ApiAdapter(); + DigitalOceanDnsProvider.load( + final bool isAuthotized, + ) : _adapter = ApiAdapter( + isWithToken: isAuthotized, + ); + + ApiAdapter _adapter; + + @override + Future> tryInitApiByToken(final String token) async { + final api = _adapter.api(getInitialized: false); + final result = await api.isApiTokenValid(token); + if (!result.data || !result.success) { + return result; + } + + _adapter = ApiAdapter(isWithToken: true); + return result; + } + + @override + Future> getZoneId(final String domain) async => + GenericResult( + data: domain, + success: true, + ); + + @override + Future> removeDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: null, + code: result.code, + message: result.message, + ); + } + + const ignoreType = 'SOA'; + final filteredRecords = []; + for (final record in result.data) { + if (record['type'] != ignoreType) { + filteredRecords.add(record); + } + } + + return _adapter.api().removeSimilarRecords( + domain: domain, + records: filteredRecords, + ); + } + + @override + Future>> getDnsRecords({ + required final ServerDomain domain, + }) async { + final List records = []; + final result = await _adapter.api().getDnsRecords(domain: domain); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: records, + code: result.code, + message: result.message, + ); + } + + for (final rawRecord in result.data) { + records.add( + DnsRecord( + id: rawRecord['id'], + name: rawRecord['name'], + type: rawRecord['type'], + content: rawRecord['data'], + ttl: rawRecord['ttl'], + proxied: false, + ), + ); + } + + return GenericResult(data: records, success: true); + } + + @override + Future> createDomainRecords({ + required final ServerDomain domain, + final String? ip4, + }) async => + _adapter.api().createMultipleDnsRecords( + domain: domain, + records: getProjectDnsRecords( + domain.domainName, + ip4, + ), + ); + + @override + Future> setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ) async => + _adapter.api().createMultipleDnsRecords( + domain: domain, + records: [record], + ); + + @override + Future>> domainList() async { + List domains = []; + final result = await _adapter.api().domainList(); + if (result.data.isEmpty || !result.success) { + return GenericResult( + success: result.success, + data: domains, + code: result.code, + message: result.message, + ); + } + + domains = result.data + .map( + (final el) => el['name'] as String, + ) + .toList(); + + return GenericResult( + success: true, + data: domains, + ); + } + + @override + Future>> validateDnsRecords( + final ServerDomain domain, + final String ip4, + final String dkimPublicKey, + ) async { + final GenericResult> 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.data.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.data.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 GenericResult( + data: [], + success: false, + message: e.toString(), + ); + } + return GenericResult( + data: foundRecords, + success: true, + ); + } + + List getProjectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = DnsRecord(type: 'A', name: '@', content: ip4); + + final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: '@'); + 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); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + + final DnsRecord txt1 = DnsRecord( + type: 'TXT', + name: '_dmarc', + content: 'v=DMARC1; p=none', + ttl: 18000, + ); + + final DnsRecord txt2 = DnsRecord( + type: 'TXT', + name: domainName, + content: 'v=spf1 a mx ip4:$ip4 -all', + ttl: 18000, + ); + + return [ + domainA, + apiA, + cloudA, + gitA, + meetA, + passwordA, + socialA, + mx, + txt1, + txt2, + vpn + ]; + } + + @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', + displayName: domainName, + ), + DesiredDnsRecord( + name: 'api', + content: ip4, + description: 'record.api', + displayName: 'api.$domainName', + ), + DesiredDnsRecord( + name: 'cloud', + content: ip4, + description: 'record.cloud', + displayName: 'cloud.$domainName', + ), + DesiredDnsRecord( + name: 'git', + content: ip4, + description: 'record.git', + displayName: 'git.$domainName', + ), + DesiredDnsRecord( + name: 'meet', + content: ip4, + description: 'record.meet', + displayName: 'meet.$domainName', + ), + DesiredDnsRecord( + name: 'social', + content: ip4, + description: 'record.social', + displayName: 'social.$domainName', + ), + DesiredDnsRecord( + name: 'password', + content: ip4, + description: 'record.password', + displayName: 'password.$domainName', + ), + DesiredDnsRecord( + name: 'vpn', + content: ip4, + description: 'record.vpn', + displayName: 'vpn.$domainName', + ), + const DesiredDnsRecord( + name: '@', + content: '@', + description: 'record.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + const DesiredDnsRecord( + name: '_dmarc', + 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', + content: dkimPublicKey, + description: 'record.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } +} diff --git a/lib/logic/providers/dns_providers/dns_provider_factory.dart b/lib/logic/providers/dns_providers/dns_provider_factory.dart index b42854b7..a2b1694e 100644 --- a/lib/logic/providers/dns_providers/dns_provider_factory.dart +++ b/lib/logic/providers/dns_providers/dns_provider_factory.dart @@ -1,7 +1,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/providers/dns_providers/cloudflare.dart'; import 'package:selfprivacy/logic/providers/dns_providers/desec.dart'; -import 'package:selfprivacy/logic/providers/dns_providers/digital_ocean.dart'; +import 'package:selfprivacy/logic/providers/dns_providers/digital_ocean_dns.dart'; import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart'; import 'package:selfprivacy/logic/providers/provider_settings.dart';