import 'dart:io'; import 'package:dio/dio.dart'; 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'; class DesecApi extends DnsProviderApi { DesecApi({ this.hasLogger = false, this.isWithToken = true, this.customToken, }); @override final bool hasLogger; @override final bool isWithToken; final String? customToken; @override RegExp getApiTokenValidation() => RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); @override BaseOptions get options { final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { final String? token = getIt().dnsProviderKey; assert(token != null); options.headers = {'Authorization': 'Token $token'}; } if (customToken != null) { options.headers = {'Authorization': 'Token $customToken'}; } if (validateStatus != null) { options.validateStatus = validateStatus!; } return options; } @override String rootAddress = 'https://desec.io/api/v1/domains/'; Future> isApiTokenValid(final String token) async { bool isValid = false; Response? response; String message = ''; final Dio client = await getClient(); try { response = await client.get( '', options: Options( followRedirects: false, validateStatus: (final status) => status != null && (status >= 200 || status == 401), headers: {'Authorization': 'Token $token'}, ), ); await Future.delayed(const Duration(seconds: 1)); } catch (e) { print(e); isValid = false; message = e.toString(); } finally { close(client); } if (response == null) { return GenericResult( data: isValid, success: false, message: message, ); } if (response.statusCode == HttpStatus.ok) { isValid = true; } else if (response.statusCode == HttpStatus.unauthorized) { isValid = false; } else { throw Exception('code: ${response.statusCode}'); } return GenericResult( data: isValid, success: true, message: response.statusMessage, ); } Future> updateRecords({ required final ServerDomain domain, required final List records, }) async { final String domainName = domain.domainName; final String url = '/$domainName/rrsets/'; final Dio client = await getClient(); try { await client.put(url, data: records); await Future.delayed(const Duration(seconds: 1)); } catch (e) { print(e); return GenericResult( success: false, data: null, message: e.toString(), ); } finally { close(client); } return GenericResult(success: true, data: null); } Future>> getDnsRecords({ required final ServerDomain domain, }) async { Response? response; final String domainName = domain.domainName; List allRecords = []; final String url = '/$domainName/rrsets/'; final Dio client = await getClient(); try { response = await client.get(url); await Future.delayed(const Duration(seconds: 1)); allRecords = response.data; } catch (e) { print(e); return GenericResult( data: allRecords, success: false, message: e.toString(), code: response?.statusCode, ); } finally { close(client); } return GenericResult(data: allRecords, success: true); } Future> createRecords({ required final ServerDomain domain, required final List records, }) async { final String domainName = domain.domainName; final String url = '/$domainName/rrsets/'; final Dio client = await getClient(); try { await client.post(url, data: records); await Future.delayed(const Duration(seconds: 1)); } catch (e) { print(e); return GenericResult( success: false, data: null, message: e.toString(), ); } finally { close(client); } return GenericResult(success: true, data: null); } String? extractContent(final DnsRecord record) { String? content = record.content; if (record.type == 'TXT' && content != null && !content.startsWith('"')) { content = '"$content"'; } return content; } @override Future> getDomains() async { List domains = []; late final Response? response; final Dio client = await getClient(); try { response = await client.get( '', ); await Future.delayed(const Duration(seconds: 1)); domains = response.data; } catch (e) { print(e); return GenericResult( success: false, data: domains, code: response?.statusCode, message: response?.statusMessage, ); } finally { close(client); } return GenericResult( success: true, data: domains, code: response.statusCode, message: response.statusMessage, ); } @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}.${domain.domainName}' == 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 GenericResult( data: [], success: false, message: e.toString(), ); } return GenericResult( 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, ), ]; } }