feat: Implement polymorphic DNS check for DNS API

This commit is contained in:
NaiJi 2023-05-16 11:06:01 -03:00
parent 56dd40e90e
commit e9665ad75d
6 changed files with 385 additions and 172 deletions

View file

@ -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/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class CloudflareApi extends DnsProviderApi { class CloudflareApi extends DnsProviderApi {
CloudflareApi({ CloudflareApi({
@ -317,4 +318,147 @@ class CloudflareApi extends DnsProviderApi {
return domains; return domains;
} }
@override
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final List<DnsRecord> records = await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DesiredDnsRecord> 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,
),
];
}
} }

View file

@ -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/api_maps/rest_maps/dns_providers/dns_provider.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart'; import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/utils/network_utils.dart';
class DesecApi extends DnsProviderApi { class DesecApi extends DnsProviderApi {
DesecApi({ DesecApi({
@ -61,6 +62,7 @@ class DesecApi extends DnsProviderApi {
headers: {'Authorization': 'Token $token'}, headers: {'Authorization': 'Token $token'},
), ),
); );
await Future.delayed(const Duration(seconds: 1));
} catch (e) { } catch (e) {
print(e); print(e);
isValid = false; isValid = false;
@ -102,13 +104,29 @@ class DesecApi extends DnsProviderApi {
}) async { }) async {
final String domainName = domain.domainName; final String domainName = domain.domainName;
final String url = '/$domainName/rrsets/'; final String url = '/$domainName/rrsets/';
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
final Response response = await client.get(url); final List<dynamic> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) {
final List records = response.data; bulkRecords.add(
await client.put(url, data: records); 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) { } catch (e) {
print(e); print(e);
return APIGenericResult( return APIGenericResult(
@ -136,14 +154,18 @@ class DesecApi extends DnsProviderApi {
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
response = await client.get(url); response = await client.get(url);
await Future.delayed(const Duration(seconds: 1));
final List records = response.data; final List records = response.data;
for (final record in records) { for (final record in records) {
final String? content = (record['records'] is List<dynamic>)
? record['records'][0]
: record['records'];
allRecords.add( allRecords.add(
DnsRecord( DnsRecord(
name: record['subname'], name: record['subname'],
type: record['type'], type: record['type'],
content: record['records'], content: content,
ttl: record['ttl'], ttl: record['ttl'],
), ),
); );
@ -164,15 +186,13 @@ class DesecApi extends DnsProviderApi {
}) async { }) async {
final String domainName = domain.domainName; final String domainName = domain.domainName;
final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4); final List<DnsRecord> listDnsRecords = projectDnsRecords(domainName, ip4);
final List<Future> allCreateFutures = <Future>[];
final Dio client = await getClient(); final Dio client = await getClient();
try { try {
final List<dynamic> bulkRecords = [];
for (final DnsRecord record in listDnsRecords) { for (final DnsRecord record in listDnsRecords) {
allCreateFutures.add( bulkRecords.add(
client.post( record.name == null
'/$domainName/rrsets/',
data: record.name == null
? { ? {
'type': record.type, 'type': record.type,
'ttl': record.ttl, 'ttl': record.ttl,
@ -184,10 +204,13 @@ class DesecApi extends DnsProviderApi {
'ttl': record.ttl, 'ttl': record.ttl,
'records': [record.content], '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) { } on DioError catch (e) {
print(e.message); print(e.message);
rethrow; rethrow;
@ -209,9 +232,10 @@ class DesecApi extends DnsProviderApi {
final String? domainName, final String? domainName,
final String? ip4, 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 apiA = DnsRecord(type: 'A', name: 'api', content: ip4);
final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4);
final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4);
@ -225,14 +249,14 @@ class DesecApi extends DnsProviderApi {
final DnsRecord txt1 = DnsRecord( final DnsRecord txt1 = DnsRecord(
type: 'TXT', type: 'TXT',
name: '_dmarc', name: '_dmarc',
content: 'v=DMARC1; p=none', content: '"v=DMARC1; p=none"',
ttl: 18000, ttl: 18000,
); );
final DnsRecord txt2 = DnsRecord( final DnsRecord txt2 = DnsRecord(
type: 'TXT', type: 'TXT',
name: null, name: '',
content: 'v=spf1 a mx ip4:$ip4 -all', content: '"v=spf1 a mx ip4:$ip4 -all"',
ttl: 18000, ttl: 18000,
); );
@ -275,6 +299,7 @@ class DesecApi extends DnsProviderApi {
'records': [record.content], 'records': [record.content],
}, },
); );
await Future.delayed(const Duration(seconds: 1));
} catch (e) { } catch (e) {
print(e); print(e);
} finally { } finally {
@ -291,6 +316,7 @@ class DesecApi extends DnsProviderApi {
final Response response = await client.get( final Response response = await client.get(
'', '',
); );
await Future.delayed(const Duration(seconds: 1));
domains = response.data domains = response.data
.map<String>((final el) => el['name'] as String) .map<String>((final el) => el['name'] as String)
.toList(); .toList();
@ -302,4 +328,148 @@ class DesecApi extends DnsProviderApi {
return domains; return domains;
} }
@override
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
) async {
final List<DnsRecord> records = await getDnsRecords(domain: domain);
final List<DesiredDnsRecord> foundRecords = [];
try {
final List<DesiredDnsRecord> 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<DesiredDnsRecord> 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,
),
];
}
} }

View file

@ -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/api_maps/rest_maps/api_map.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.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'; export 'package:selfprivacy/logic/api_maps/api_generic_result.dart';
@ -26,9 +27,18 @@ abstract class DnsProviderApi extends ApiMap {
final DnsRecord record, final DnsRecord record,
final ServerDomain domain, final ServerDomain domain,
); );
Future<APIGenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
);
List<DesiredDnsRecord> getDesiredDnsRecords(
final String? domainName,
final String? ip4,
final String? dkimPublicKey,
);
Future<String?> getZoneId(final String domain); Future<String?> getZoneId(final String domain);
Future<List<String>> domainList(); Future<List<String>> domainList();
Future<APIGenericResult<bool>> isApiTokenValid(final String token); Future<APIGenericResult<bool>> isApiTokenValid(final String token);
RegExp getApiTokenValidation(); RegExp getApiTokenValidation();
} }

View file

@ -394,7 +394,7 @@ class HetznerApi extends ServerProviderApi with VolumeProviderApi {
final String apiToken = StringGenerators.apiToken(); final String apiToken = StringGenerators.apiToken();
final String hostname = getHostnameFromDomain(domainName); final String hostname = getHostnameFromDomain(domainName);
const String infectBranch = 'providers/hetzner'; const String infectBranch = 'testing/desec';
final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false'; final String stagingAcme = StagingOptions.stagingAcme ? 'true' : 'false';
final String base64Password = final String base64Password =
base64.encode(utf8.encode(rootUser.password ?? 'PASS')); base64.encode(utf8.encode(rootUser.password ?? 'PASS'));

View file

@ -25,7 +25,9 @@ class DnsRecordsCubit
emit( emit(
DnsRecordsState( DnsRecordsState(
dnsState: DnsRecordsStatus.refreshing, dnsState: DnsRecordsStatus.refreshing,
dnsRecords: getDesiredDnsRecords( dnsRecords: ApiController.currentDnsProviderApiFactory!
.getDnsProvider()
.getDesiredDnsRecords(
serverInstallationCubit.state.serverDomain?.domainName, serverInstallationCubit.state.serverDomain?.domainName,
'', '',
'', '',
@ -37,64 +39,32 @@ class DnsRecordsCubit
final ServerDomain? domain = serverInstallationCubit.state.serverDomain; final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress = final String? ipAddress =
serverInstallationCubit.state.serverDetails?.ip4; serverInstallationCubit.state.serverDetails?.ip4;
if (domain != null && ipAddress != null) { if (domain == null && ipAddress == null) {
final List<DnsRecord> records = await ApiController emit(const DnsRecordsState());
.currentDnsProviderApiFactory! return;
}
final foundRecords = await ApiController.currentDnsProviderApiFactory!
.getDnsProvider() .getDnsProvider()
.getDnsRecords(domain: domain); .validateDnsRecords(
final String? dkimPublicKey = domain!,
extractDkimRecord(await api.getDnsRecords())?.content; ipAddress!,
final List<DesiredDnsRecord> desiredRecords = extractDkimRecord(await api.getDnsRecords())?.content ?? '',
getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey);
final List<DesiredDnsRecord> 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 if (!foundRecords.success || foundRecords.data.isEmpty) {
// to compare them emit(const DnsRecordsState());
final String? foundContent = return;
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( emit(
DnsRecordsState( DnsRecordsState(
dnsRecords: foundRecords, dnsRecords: foundRecords.data,
dnsState: foundRecords.any((final r) => r.isSatisfied == false) dnsState: foundRecords.data.any((final r) => r.isSatisfied == false)
? DnsRecordsStatus.error ? DnsRecordsStatus.error
: DnsRecordsStatus.good, : DnsRecordsStatus.good,
), ),
); );
} else {
emit(const DnsRecordsState());
}
} }
} }

View file

@ -41,87 +41,6 @@ class DesiredDnsRecord {
); );
} }
List<DesiredDnsRecord> 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<DnsRecord> records) { DnsRecord? extractDkimRecord(final List<DnsRecord> records) {
DnsRecord? dkimRecord; DnsRecord? dkimRecord;