refactor: Move DNS records validation to cubit layer

pull/392/head
NaiJi ✨ 2023-12-29 23:58:58 +04:00
parent ada5f1a66c
commit 1c07476764
15 changed files with 156 additions and 329 deletions

View File

@ -5529,11 +5529,10 @@ extension ClientExtension$Mutation$RemoveRepository on graphql.GraphQLClient {
mutate$RemoveRepository(
[Options$Mutation$RemoveRepository? options]) async =>
await this.mutate(options ?? Options$Mutation$RemoveRepository());
graphql.ObservableQuery<Mutation$RemoveRepository>
watchMutation$RemoveRepository(
[WatchOptions$Mutation$RemoveRepository? options]) =>
this.watchMutation(
options ?? WatchOptions$Mutation$RemoveRepository());
graphql.ObservableQuery<
Mutation$RemoveRepository> watchMutation$RemoveRepository(
[WatchOptions$Mutation$RemoveRepository? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$RemoveRepository());
}
class Mutation$RemoveRepository$backup {

View File

@ -150,6 +150,7 @@ type DnsRecord {
recordType: String!
name: String!
content: String!
displayName: String!
ttl: Int!
priority: Int
}

View File

@ -4440,11 +4440,10 @@ extension ClientExtension$Mutation$RunSystemRebuild on graphql.GraphQLClient {
mutate$RunSystemRebuild(
[Options$Mutation$RunSystemRebuild? options]) async =>
await this.mutate(options ?? Options$Mutation$RunSystemRebuild());
graphql.ObservableQuery<Mutation$RunSystemRebuild>
watchMutation$RunSystemRebuild(
[WatchOptions$Mutation$RunSystemRebuild? options]) =>
this.watchMutation(
options ?? WatchOptions$Mutation$RunSystemRebuild());
graphql.ObservableQuery<
Mutation$RunSystemRebuild> watchMutation$RunSystemRebuild(
[WatchOptions$Mutation$RunSystemRebuild? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$RunSystemRebuild());
}
class Mutation$RunSystemRebuild$runSystemRebuild
@ -4883,11 +4882,10 @@ extension ClientExtension$Mutation$RunSystemRollback on graphql.GraphQLClient {
mutate$RunSystemRollback(
[Options$Mutation$RunSystemRollback? options]) async =>
await this.mutate(options ?? Options$Mutation$RunSystemRollback());
graphql.ObservableQuery<Mutation$RunSystemRollback>
watchMutation$RunSystemRollback(
[WatchOptions$Mutation$RunSystemRollback? options]) =>
this.watchMutation(
options ?? WatchOptions$Mutation$RunSystemRollback());
graphql.ObservableQuery<
Mutation$RunSystemRollback> watchMutation$RunSystemRollback(
[WatchOptions$Mutation$RunSystemRollback? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$RunSystemRollback());
}
class Mutation$RunSystemRollback$runSystemRollback
@ -5324,11 +5322,10 @@ extension ClientExtension$Mutation$RunSystemUpgrade on graphql.GraphQLClient {
mutate$RunSystemUpgrade(
[Options$Mutation$RunSystemUpgrade? options]) async =>
await this.mutate(options ?? Options$Mutation$RunSystemUpgrade());
graphql.ObservableQuery<Mutation$RunSystemUpgrade>
watchMutation$RunSystemUpgrade(
[WatchOptions$Mutation$RunSystemUpgrade? options]) =>
this.watchMutation(
options ?? WatchOptions$Mutation$RunSystemUpgrade());
graphql.ObservableQuery<
Mutation$RunSystemUpgrade> watchMutation$RunSystemUpgrade(
[WatchOptions$Mutation$RunSystemUpgrade? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$RunSystemUpgrade());
}
class Mutation$RunSystemUpgrade$runSystemUpgrade
@ -11384,11 +11381,10 @@ extension ClientExtension$Mutation$GetNewDeviceApiKey on graphql.GraphQLClient {
mutate$GetNewDeviceApiKey(
[Options$Mutation$GetNewDeviceApiKey? options]) async =>
await this.mutate(options ?? Options$Mutation$GetNewDeviceApiKey());
graphql.ObservableQuery<Mutation$GetNewDeviceApiKey>
watchMutation$GetNewDeviceApiKey(
[WatchOptions$Mutation$GetNewDeviceApiKey? options]) =>
this.watchMutation(
options ?? WatchOptions$Mutation$GetNewDeviceApiKey());
graphql.ObservableQuery<
Mutation$GetNewDeviceApiKey> watchMutation$GetNewDeviceApiKey(
[WatchOptions$Mutation$GetNewDeviceApiKey? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$GetNewDeviceApiKey());
}
class Mutation$GetNewDeviceApiKey$getNewDeviceApiKey

View File

@ -25,6 +25,7 @@ query SystemIsUsingBinds {
fragment fragmentDnsRecords on DnsRecord {
recordType
name
displayName
content
ttl
priority

View File

@ -8,6 +8,7 @@ class Fragment$fragmentDnsRecords {
Fragment$fragmentDnsRecords({
required this.recordType,
required this.name,
required this.displayName,
required this.content,
required this.ttl,
this.priority,
@ -17,6 +18,7 @@ class Fragment$fragmentDnsRecords {
factory Fragment$fragmentDnsRecords.fromJson(Map<String, dynamic> json) {
final l$recordType = json['recordType'];
final l$name = json['name'];
final l$displayName = json['displayName'];
final l$content = json['content'];
final l$ttl = json['ttl'];
final l$priority = json['priority'];
@ -24,6 +26,7 @@ class Fragment$fragmentDnsRecords {
return Fragment$fragmentDnsRecords(
recordType: (l$recordType as String),
name: (l$name as String),
displayName: (l$displayName as String),
content: (l$content as String),
ttl: (l$ttl as int),
priority: (l$priority as int?),
@ -35,6 +38,8 @@ class Fragment$fragmentDnsRecords {
final String name;
final String displayName;
final String content;
final int ttl;
@ -49,6 +54,8 @@ class Fragment$fragmentDnsRecords {
_resultData['recordType'] = l$recordType;
final l$name = name;
_resultData['name'] = l$name;
final l$displayName = displayName;
_resultData['displayName'] = l$displayName;
final l$content = content;
_resultData['content'] = l$content;
final l$ttl = ttl;
@ -64,6 +71,7 @@ class Fragment$fragmentDnsRecords {
int get hashCode {
final l$recordType = recordType;
final l$name = name;
final l$displayName = displayName;
final l$content = content;
final l$ttl = ttl;
final l$priority = priority;
@ -71,6 +79,7 @@ class Fragment$fragmentDnsRecords {
return Object.hashAll([
l$recordType,
l$name,
l$displayName,
l$content,
l$ttl,
l$priority,
@ -97,6 +106,11 @@ class Fragment$fragmentDnsRecords {
if (l$name != lOther$name) {
return false;
}
final l$displayName = displayName;
final lOther$displayName = other.displayName;
if (l$displayName != lOther$displayName) {
return false;
}
final l$content = content;
final lOther$content = other.content;
if (l$content != lOther$content) {
@ -142,6 +156,7 @@ abstract class CopyWith$Fragment$fragmentDnsRecords<TRes> {
TRes call({
String? recordType,
String? name,
String? displayName,
String? content,
int? ttl,
int? priority,
@ -165,6 +180,7 @@ class _CopyWithImpl$Fragment$fragmentDnsRecords<TRes>
TRes call({
Object? recordType = _undefined,
Object? name = _undefined,
Object? displayName = _undefined,
Object? content = _undefined,
Object? ttl = _undefined,
Object? priority = _undefined,
@ -177,6 +193,9 @@ class _CopyWithImpl$Fragment$fragmentDnsRecords<TRes>
name: name == _undefined || name == null
? _instance.name
: (name as String),
displayName: displayName == _undefined || displayName == null
? _instance.displayName
: (displayName as String),
content: content == _undefined || content == null
? _instance.content
: (content as String),
@ -198,6 +217,7 @@ class _CopyWithStubImpl$Fragment$fragmentDnsRecords<TRes>
call({
String? recordType,
String? name,
String? displayName,
String? content,
int? ttl,
int? priority,
@ -229,6 +249,13 @@ const fragmentDefinitionfragmentDnsRecords = FragmentDefinitionNode(
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: 'displayName'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: 'content'),
alias: null,

View File

@ -33,16 +33,16 @@ class DnsRecordsCubit
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress =
serverInstallationCubit.state.serverDetails?.ip4;
if (domain == null && ipAddress == null) {
if (domain == null || ipAddress == null) {
emit(const DnsRecordsState());
return;
}
final List<DnsRecord> allDnsRecords = await api.getDnsRecords();
allDnsRecords.removeWhere((final record) => record.type == 'AAAA');
final foundRecords =
await ProvidersController.currentDnsProvider!.validateDnsRecords(
domain!,
ipAddress!,
final foundRecords = await validateDnsRecords(
domain,
extractDkimRecord(allDnsRecords)?.content ?? '',
allDnsRecords,
);
@ -63,6 +63,93 @@ class DnsRecordsCubit
}
}
/// Tries to check whether all known DNS records on the domain by ip4
/// match expectations of SelfPrivacy in order to launch.
///
/// Will return list of [DesiredDnsRecord] objects, which represent
/// only those records which have successfully passed validation.
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String dkimPublicKey,
final List<DnsRecord> pendingDnsRecords,
) async {
final result = await ProvidersController.currentDnsProvider!
.getDnsRecords(domain: domain);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: [],
code: result.code,
message: result.message,
);
}
final List<DnsRecord> providerDnsRecords = result.data;
final List<DesiredDnsRecord> foundRecords = [];
try {
for (final DnsRecord pendingDnsRecord in pendingDnsRecords) {
if (pendingDnsRecord.name == 'selector._domainkey') {
final foundRecord = providerDnsRecords.firstWhere(
(final r) =>
(r.name == pendingDnsRecord.name) &&
r.type == pendingDnsRecord.type,
orElse: () => DnsRecord(
displayName: pendingDnsRecord.displayName,
name: pendingDnsRecord.name,
type: pendingDnsRecord.type,
content: pendingDnsRecord.content,
ttl: pendingDnsRecord.ttl,
),
);
final String foundContent =
foundRecord.content!.replaceAll(RegExp(r'\s+'), '').trim();
final String desiredContent =
pendingDnsRecord.content!.replaceAll(RegExp(r'\s+'), '').trim();
final isSatisfied = (desiredContent == foundContent);
foundRecords.add(
DesiredDnsRecord(
name: pendingDnsRecord.name!,
displayName: pendingDnsRecord.displayName,
content: pendingDnsRecord.content!,
isSatisfied: isSatisfied,
type: pendingDnsRecord.type,
category: DnsRecordsCategory.email,
),
);
} else {
final foundMatch = providerDnsRecords.any(
(final r) =>
r.name == pendingDnsRecord.name &&
r.type == pendingDnsRecord.type &&
r.content == pendingDnsRecord.content,
);
foundRecords.add(
DesiredDnsRecord(
name: pendingDnsRecord.name!,
displayName: pendingDnsRecord.displayName,
content: pendingDnsRecord.content!,
isSatisfied: foundMatch,
category: pendingDnsRecord.type == 'A'
? DnsRecordsCategory.services
: DnsRecordsCategory.email,
),
);
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: foundRecords,
success: true,
);
}
@override
void onChange(final Change<DnsRecordsState> change) {
// print(change);

View File

@ -34,13 +34,17 @@ DnsRecord _toDnsRecord(
String name = desecRecord.subname;
int? priority;
if (type == 'MX') {
name = name.isEmpty ? '@' : name;
name = name.isEmpty ? domainName : name;
final contentBulk = content.split(' ');
content = contentBulk[1];
if (content.contains(domainName)) {
content =
content.substring(0, content.length - 1); // cut away trailing dot
}
priority = int.parse(contentBulk[0]);
}
if (type == 'TXT' && content.isNotEmpty && content.startsWith('"')) {
content = content.substring(1, content.length); // cut away quotes
content = content.substring(1, content.length - 1); // cut away quotes
}
if (name.isEmpty) {
name = domainName;

View File

@ -27,9 +27,7 @@ DnsRecord _toDnsRecord(
String convert(final String entry) => (entry == '@') ? rootDomain : entry;
String name = digitalOceanRecord.name;
final String content = convert(digitalOceanRecord.data);
if (type != 'MX') {
name = convert(name);
}
name = (name == '@') ? rootDomain : name;
return DnsRecord(
name: name,
content: content,

View File

@ -9,6 +9,7 @@ class DnsRecord {
required this.type,
required this.name,
required this.content,
this.displayName,
this.ttl = 3600,
this.priority = 10,
this.proxied = false,
@ -19,12 +20,14 @@ class DnsRecord {
) : this(
type: record.recordType,
name: record.name,
displayName: record.displayName,
content: record.content,
ttl: record.ttl,
priority: record.priority ?? 10,
);
final String type;
final String? displayName;
final String? name;
final String? content;
final int ttl;

View File

@ -8,6 +8,7 @@ part of 'dns_records.dart';
Map<String, dynamic> _$DnsRecordToJson(DnsRecord instance) => <String, dynamic>{
'type': instance.type,
'displayName': instance.displayName,
'name': instance.name,
'content': instance.content,
'ttl': instance.ttl,

View File

@ -1,5 +1,4 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
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';
@ -180,103 +179,6 @@ class CloudflareDnsProvider extends DnsProvider {
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
final List<DnsRecord> pendingDnsRecords,
) async {
final syncZoneIdResult = await syncZoneId(domain.domainName);
if (!syncZoneIdResult.success) {
return GenericResult(
success: syncZoneIdResult.success,
data: [],
code: syncZoneIdResult.code,
message: syncZoneIdResult.message,
);
}
final result =
await _adapter.api().getDnsRecords(zoneId: _adapter.cachedZoneId);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: [],
code: result.code,
message: result.message,
);
}
final records = result.data;
final List<DesiredDnsRecord> foundRecords = [];
try {
for (final DnsRecord pendingDnsRecord in pendingDnsRecords) {
final record = CloudflareDnsRecord.fromDnsRecord(
pendingDnsRecord,
domain.domainName,
);
if (record.name == 'selector._domainkey') {
final CloudflareDnsRecord foundRecord = records.firstWhere(
(final r) => (r.name == record.name) && r.type == record.type,
orElse: () => CloudflareDnsRecord(
zoneName: domain.domainName,
name: record.name,
type: record.type,
content: '',
ttl: 800,
),
);
// 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+'), '') ?? '';
foundRecords.add(
DesiredDnsRecord(
name: record.name ?? '',
description: record.name?.split('.')[0] ?? '',
content: record.content ?? '',
isSatisfied: foundContent == content,
type: record.type,
category: DnsRecordsCategory.email,
),
);
} else {
final foundMatch = records.any(
(final r) =>
(r.name == record.name) &&
r.type == record.type &&
r.content == record.content,
);
foundRecords.add(
DesiredDnsRecord(
name: record.name ?? '',
description: record.name?.split('.')[0] ?? '',
content: record.content ?? '',
isSatisfied: foundMatch,
type: record.type,
category: record.type == 'A'
? DnsRecordsCategory.services
: DnsRecordsCategory.email,
),
);
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: foundRecords,
success: true,
);
}
Future<GenericResult<void>> syncZoneId(final String domain) async {
if (domain == _adapter.cachedDomain && _adapter.cachedZoneId.isNotEmpty) {
return GenericResult(

View File

@ -1,5 +1,4 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
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';
@ -181,91 +180,4 @@ class DesecDnsProvider extends DnsProvider {
data: null,
);
}
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
final List<DnsRecord> pendingDnsRecords,
) async {
final result = await _adapter.api().getDnsRecords(domain.domainName);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: [],
code: result.code,
message: result.message,
);
}
final records = result.data;
final List<DesiredDnsRecord> foundRecords = [];
try {
for (final DnsRecord pendingDnsRecord in pendingDnsRecords) {
final record = DesecDnsRecord.fromDnsRecord(
pendingDnsRecord,
domain.domainName,
);
if (record.subname == 'selector._domainkey') {
final DesecDnsRecord foundRecord = records.firstWhere(
(final r) =>
('${r.subname}.${domain.domainName}' == record.subname) &&
r.type == record.type,
orElse: () => DesecDnsRecord(
subname: record.subname,
type: record.type,
records: [],
ttl: record.ttl,
),
);
final desecRecords = foundRecord.records;
final content = desecRecords.isEmpty ? '' : desecRecords[0];
final String foundContent = content.replaceAll(RegExp(r'\s+'), '');
final String desiredContent =
record.records[0].replaceAll(RegExp(r'\s+'), '');
foundRecords.add(
DesiredDnsRecord(
name: '${record.subname}.${domain.domainName}',
description:
record.subname.isEmpty ? record.subname : domain.domainName,
content: record.records[0],
isSatisfied: foundContent == desiredContent,
type: record.type,
category: DnsRecordsCategory.email,
),
);
} else {
final foundMatch = records.any(
(final r) =>
r.subname == record.subname &&
r.type == record.type &&
r.records[0] == record.records[0],
);
foundRecords.add(
DesiredDnsRecord(
name: '${record.subname}.${domain.domainName}',
description: record.subname,
content: record.records[0],
isSatisfied: foundMatch,
category: record.type == 'A'
? DnsRecordsCategory.services
: DnsRecordsCategory.email,
),
);
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: foundRecords,
success: true,
);
}
}

View File

@ -1,4 +1,3 @@
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/digital_ocean_dns/digital_ocean_dns_api.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_providers/digital_ocean_dns_info.dart';
@ -157,90 +156,4 @@ class DigitalOceanDnsProvider extends DnsProvider {
),
],
);
@override
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
final List<DnsRecord> pendingDnsRecords,
) async {
final result = await _adapter.api().getDnsRecords(domain.domainName);
if (result.data.isEmpty || !result.success) {
return GenericResult(
success: result.success,
data: [],
code: result.code,
message: result.message,
);
}
final records = result.data;
final List<DesiredDnsRecord> foundRecords = [];
try {
for (final DnsRecord pendingDnsRecord in pendingDnsRecords) {
final record = DigitalOceanDnsRecord.fromDnsRecord(
pendingDnsRecord,
domain.domainName,
);
if (record.name == 'selector._domainkey') {
final DigitalOceanDnsRecord foundRecord = records.firstWhere(
(final r) => (r.name == record.name) && r.type == record.type,
orElse: () => DigitalOceanDnsRecord(
id: null,
name: record.name,
type: record.type,
data: '',
ttl: 800,
),
);
// remove all spaces and tabulators from
// the foundRecord.content and the record.content
// to compare them
final String foundContent =
foundRecord.data.replaceAll(RegExp(r'\s+'), '');
final String content = record.data.replaceAll(RegExp(r'\s+'), '');
foundRecords.add(
DesiredDnsRecord(
name: record.name,
description: record.name == '@' ? domain.domainName : record.name,
content: record.data,
isSatisfied: foundContent == content,
type: record.type,
category: DnsRecordsCategory.email,
),
);
} else {
final foundMatch = records.any(
(final r) =>
r.name == record.name &&
r.type == record.type &&
r.data == record.data,
);
foundRecords.add(
DesiredDnsRecord(
name: record.name,
description: record.name == '@' ? domain.domainName : record.name,
content: record.data,
isSatisfied: foundMatch,
type: record.type,
category: record.type == 'A'
? DnsRecordsCategory.services
: DnsRecordsCategory.email,
),
);
}
}
} catch (e) {
print(e);
return GenericResult(
data: [],
success: false,
message: e.toString(),
);
}
return GenericResult(
data: foundRecords,
success: true,
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
export 'package:selfprivacy/logic/api_maps/generic_result.dart';
@ -57,17 +56,4 @@ abstract class DnsProvider {
final DnsRecord record,
final ServerDomain domain,
);
/// Tries to check whether all known DNS records on the domain by ip4
/// match expectations of SelfPrivacy in order to launch.
///
/// Will return list of [DesiredDnsRecord] objects, which represent
/// only those records which have successfully passed validation.
/// TODO: Unify across DNS providers
Future<GenericResult<List<DesiredDnsRecord>>> validateDnsRecords(
final ServerDomain domain,
final String ip4,
final String dkimPublicKey,
final List<DnsRecord> pendingDnsRecords,
);
}

View File

@ -151,10 +151,8 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
? neutralColor
: errorColor,
),
title: Text(dnsRecord.description),
subtitle: Text(
dnsRecord.displayName ?? dnsRecord.name,
),
title: Text(dnsRecord.displayName ?? dnsRecord.name),
subtitle: Text(dnsRecord.content),
),
],
),
@ -193,9 +191,8 @@ class _DnsDetailsPageState extends State<DnsDetailsPage> {
? neutralColor
: errorColor,
),
title: Text(
dnsRecord.description,
),
title: Text(dnsRecord.displayName ?? dnsRecord.name),
subtitle: Text(dnsRecord.name),
),
],
),