Merge pull request 'feat: Implement better domain ownership check during installation' (#394) from domain-ownership-setup into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #394
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
pull/410/head
NaiJi ✨ 2023-12-12 17:24:22 +02:00
commit 2f6b4e0f9c
14 changed files with 163 additions and 35 deletions

View File

@ -0,0 +1,6 @@
### How to point Name Servers for Cloudflare DNS
1. Visit the following [link](https://dash.cloudflare.com) and sign
into your Cloudflare account.
2. Enter DNS settings of your domain.
3. Copy your NS records and paste them into a Nameservers section of your domain registar settings.
4. For more specific instructions for each of commonly used registars, follow the Cloudflare [guide](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/).

View File

@ -0,0 +1,6 @@
### How to point Name Servers for deSEC DNS
1. Visit the following [link](https://desec.io/domains) and sign
into your deSEC account.
2. Click on the "Setup instructions" icon on the right side of your domain card, in the "Actions" section.
3. Copy your NS records and paste them into a Nameservers section of your domain registar settings.
4. For more specific instructions follow the official deSEC guide listed on the page.

View File

@ -0,0 +1,8 @@
### How to point Name Servers for Digital Ocean DNS
1. Visit the following [link](https://cloud.digitalocean.com/) and sign
into your Digital Ocean account.
2. Enter the [Networking](https://cloud.digitalocean.com/networking/domains) tab from the menu bar on the left.
3. Make sure you are on the [Domain](https://cloud.digitalocean.com/networking/domains) section of the tab, which is the very first one.
4. Click on your domain card, the one you have selected for SelfPrivacy.
5. Copy your NS records and paste them into a Nameservers section of your domain registar settings.
6. For more specific instructions for each of commonly used registars, follow the Digital Ocean [guide](https://docs.digitalocean.com/products/networking/dns/getting-started/dns-registrars/).

View File

@ -452,6 +452,7 @@
"server_rebooted": "Server rebooted. Waiting for the last verification…",
"server_started": "Server started. It will be validated and rebooted now…",
"server_created": "Server created. DNS checks and server boot in progress…",
"domain_critical_error": "We can't reach this domain! Tap to read more…",
"until_the_next_check": "Until the next check: ",
"check": "Check",
"one_more_restart": "One more restart to apply your security certificates.",

View File

@ -23,6 +23,7 @@ import 'package:selfprivacy/logic/models/server_type.dart';
import 'package:selfprivacy/logic/providers/provider_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
import 'package:selfprivacy/utils/network_utils.dart';
export 'package:provider/provider.dart';
@ -321,13 +322,16 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
final String ip4 = dataState.serverDetails!.ip4;
final String domainName = dataState.serverDomain!.domainName;
final Map<String, bool> matches = await repository.isDnsAddressesMatch(
final Map<String, DnsRecordStatus> matches = await validateDnsMatch(
domainName,
['api'],
ip4,
dataState.dnsMatches ?? {},
);
if (matches.values.every((final bool value) => value)) {
if (matches.values.every(
(final DnsRecordStatus value) => value == DnsRecordStatus.ok,
) &&
matches.values.isNotEmpty) {
final ServerHostingDetails server = await repository.startServer(
dataState.serverDetails!,
);

View File

@ -212,33 +212,6 @@ class ServerInstallationRepository {
return domainResult.data.contains(domain);
}
Future<Map<String, bool>> isDnsAddressesMatch(
final String? domainName,
final String? ip4,
final Map<String, bool> skippedMatches,
) async {
final Map<String, bool> matches = <String, bool>{};
try {
await InternetAddress.lookup(domainName!).then(
(final records) {
for (final record in records) {
if (skippedMatches[record.host] ?? false) {
matches[record.host] = true;
continue;
}
if (record.address == ip4!) {
matches[record.host] = true;
}
}
},
);
} catch (e) {
print(e);
}
return matches;
}
Future<void> createDkimRecord(final ServerDomain domain) async {
final ServerApi api = ServerApi();

View File

@ -145,7 +145,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
super.installationDialoguePopUp,
});
final bool isLoading;
final Map<String, bool>? dnsMatches;
final Map<String, DnsRecordStatus>? dnsMatches;
@override
List<Object?> get props => [
@ -175,7 +175,7 @@ class ServerInstallationNotFinished extends ServerInstallationState {
final bool? isServerResetedFirstTime,
final bool? isServerResetedSecondTime,
final bool? isLoading,
final Map<String, bool>? dnsMatches,
final Map<String, DnsRecordStatus>? dnsMatches,
final CallbackDialogueBranching? installationDialoguePopUp,
}) =>
ServerInstallationNotFinished(

View File

@ -38,6 +38,9 @@ class CloudflareDnsProvider extends DnsProvider {
@override
DnsProviderType get type => DnsProviderType.cloudflare;
@override
String get howToRegistar => 'how_fix_domain_cloudflare';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);

View File

@ -33,6 +33,9 @@ class DesecDnsProvider extends DnsProvider {
@override
DnsProviderType get type => DnsProviderType.desec;
@override
String get howToRegistar => 'how_fix_domain_desec';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);

View File

@ -33,6 +33,9 @@ class DigitalOceanDnsProvider extends DnsProvider {
@override
DnsProviderType get type => DnsProviderType.digitalOcean;
@override
String get howToRegistar => 'how_fix_domain_digital_ocean';
@override
Future<GenericResult<bool>> tryInitApiByToken(final String token) async {
final api = _adapter.api(getInitialized: false);

View File

@ -9,6 +9,10 @@ abstract class DnsProvider {
/// provider implements [DnsProvider] interface.
DnsProviderType get type;
/// Returns a full url to a guide on how to setup
/// DNS provider nameservers
String get howToRegistar;
/// Tries to access an account linked to the provided token.
///
/// To generate a token for your account follow instructions of your

View File

@ -0,0 +1,61 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/logic/providers/dns_providers/dns_provider.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
class BrokenDomainOutlinedCard extends StatelessWidget {
const BrokenDomainOutlinedCard({
required this.domain,
required this.dnsProvider,
super.key,
});
final String domain;
final DnsProvider dnsProvider;
@override
Widget build(final BuildContext context) => SizedBox(
width: double.infinity,
child: FilledCard(
error: true,
child: InkResponse(
highlightShape: BoxShape.rectangle,
onTap: () => context.read<SupportSystemCubit>().showArticle(
article: dnsProvider.howToRegistar,
context: context,
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
size: 24.0,
),
const SizedBox(width: 12.0),
Flexible(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
domain,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyLarge,
),
Text('initializing.domain_critical_error'.tr()),
],
),
),
],
),
),
),
),
);
}

View File

@ -10,6 +10,7 @@ import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_fo
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_setup_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart';
import 'package:selfprivacy/ui/components/buttons/outlined_button.dart';
@ -17,6 +18,7 @@ import 'package:selfprivacy/ui/components/drawers/progress_drawer.dart';
import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart';
import 'package:selfprivacy/ui/components/drawers/support_drawer.dart';
import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/broken_domain_outlined_card.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/domain_picker.dart';
import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart';
@ -24,6 +26,7 @@ import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart'
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart';
import 'package:selfprivacy/ui/router/router.dart';
import 'package:selfprivacy/utils/breakpoints.dart';
import 'package:selfprivacy/utils/network_utils.dart';
@RoutePage()
class InitializingPage extends StatelessWidget {
@ -493,12 +496,17 @@ class InitializingPage extends StatelessWidget {
Column(
children: state.dnsMatches!.entries.map((final entry) {
final String domain = entry.key;
final bool isCorrect = entry.value;
if (entry.value == DnsRecordStatus.nonexistent) {
return BrokenDomainOutlinedCard(
domain: domain,
dnsProvider: ProvidersController.currentDnsProvider!,
);
}
return Row(
children: [
if (isCorrect)
if (entry.value == DnsRecordStatus.ok)
const Icon(Icons.check, color: Colors.green),
if (!isCorrect)
if (entry.value == DnsRecordStatus.waiting)
const Icon(Icons.schedule, color: Colors.amber),
const SizedBox(width: 10),
Text(domain),

View File

@ -1,6 +1,54 @@
import 'dart:io';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:url_launcher/url_launcher.dart';
enum DnsRecordStatus { ok, waiting, nonexistent }
/// Check if DNS records were recognized.
///
/// Return pairs of full record name matched to its status.
///
/// If no record found, return just one pair of [domain] matched to critical non-existent status.
///
/// - [domain] - full domain delegated to SelfPrivacy (e.g. reimu.love)
/// - [subdomains] - list of all subdomains we want to validate recods of (e.g. api, cloud...)
/// - [ip4] - IP address of our server we want to validate DNS records by (e.g. 127.0.0.1)
Future<Map<String, DnsRecordStatus>> validateDnsMatch(
final String domain,
final List<String> subdomains,
final String ip4,
) async {
final Map<String, DnsRecordStatus> matches = <String, DnsRecordStatus>{};
Future<void> lookup(final String address) async {
await InternetAddress.lookup(address).then(
(final records) {
for (final record in records) {
final bool isIpCorrect = record.address == ip4;
matches[record.host] =
isIpCorrect ? DnsRecordStatus.ok : DnsRecordStatus.waiting;
}
},
);
}
try {
await lookup(domain);
for (final subdomain in subdomains) {
await lookup('$subdomain.$domain');
}
} catch (e) {
print(e);
}
if (matches.isEmpty) {
matches[domain] = DnsRecordStatus.nonexistent;
}
return matches;
}
DnsRecord? extractDkimRecord(final List<DnsRecord> records) {
DnsRecord? dkimRecord;