mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-08 17:11:14 +00:00
Merge pull request 'feat: Implement better domain ownership check during installation' (#394) from domain-ownership-setup into master
Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/pulls/394 Reviewed-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
commit
2f6b4e0f9c
6
assets/markdown/how_fix_domain_cloudflare-en.md
Normal file
6
assets/markdown/how_fix_domain_cloudflare-en.md
Normal 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/).
|
6
assets/markdown/how_fix_domain_desec-en.md
Normal file
6
assets/markdown/how_fix_domain_desec-en.md
Normal 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.
|
8
assets/markdown/how_fix_domain_digital_ocean-en.md
Normal file
8
assets/markdown/how_fix_domain_digital_ocean-en.md
Normal 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/).
|
|
@ -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.",
|
||||
|
|
|
@ -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!,
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in a new issue