Merge branch 'master' into dynamic-dns
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
|
@ -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
|
@ -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.",
|
||||
|
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 264 KiB |
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 199 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.jpg
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/7.jpg
Normal file
After Width: | Height: | Size: 116 KiB |
|
@ -63,8 +63,8 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
|
|||
.state.serverDomain!.domainName
|
||||
.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
|
||||
final int serverId = serverInstallationCubit.state.serverDetails!.id;
|
||||
String bucketName = 'selfprivacy-$domain-$serverId';
|
||||
// If bucket name is too long, shorten it
|
||||
String bucketName =
|
||||
'${DateTime.now().millisecondsSinceEpoch}-$serverId-$domain';
|
||||
if (bucketName.length > 49) {
|
||||
bucketName = bucketName.substring(0, 49);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class ApiDevicesCubit
|
|||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
emit(const ApiDevicesState([], LoadingStatus.refreshing));
|
||||
emit(ApiDevicesState([state.thisDevice], LoadingStatus.refreshing));
|
||||
_refetch();
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -79,6 +79,7 @@ class HetznerServerTypeInfo {
|
|||
this.prices,
|
||||
this.name,
|
||||
this.description,
|
||||
this.architecture,
|
||||
);
|
||||
final int cores;
|
||||
final num memory;
|
||||
|
@ -86,6 +87,7 @@ class HetznerServerTypeInfo {
|
|||
|
||||
final String name;
|
||||
final String description;
|
||||
final String architecture;
|
||||
|
||||
final List<HetznerPriceInfo> prices;
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson(
|
|||
.toList(),
|
||||
json['name'] as String,
|
||||
json['description'] as String,
|
||||
json['architecture'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HetznerServerTypeInfoToJson(
|
||||
|
@ -93,6 +94,7 @@ Map<String, dynamic> _$HetznerServerTypeInfoToJson(
|
|||
'disk': instance.disk,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'architecture': instance.architecture,
|
||||
'prices': instance.prices,
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,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);
|
||||
|
|
|
@ -34,6 +34,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);
|
||||
|
|
|
@ -34,6 +34,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
|
||||
|
|
|
@ -482,6 +482,9 @@ class HetznerServerProvider extends ServerProvider {
|
|||
|
||||
final rawTypes = result.data;
|
||||
for (final rawType in rawTypes) {
|
||||
if (rawType.architecture == 'arm') {
|
||||
continue;
|
||||
}
|
||||
for (final rawPrice in rawType.prices) {
|
||||
if (rawPrice.location == location.identifier) {
|
||||
types.add(
|
||||
|
|
|
@ -91,6 +91,12 @@ class _DevicesInfo extends StatelessWidget {
|
|||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
if (devicesStatus.status == LoadingStatus.refreshing) ...[
|
||||
const Center(
|
||||
heightFactor: 4,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
...devicesStatus.otherDevices
|
||||
.map((final device) => _DeviceTile(device: device)),
|
||||
],
|
||||
|
|
|
@ -44,10 +44,8 @@ class MorePage extends StatelessWidget {
|
|||
title: 'storage.start_migration_button'.tr(),
|
||||
iconData: Icons.drive_file_move_outline,
|
||||
goTo: () => ServicesMigrationRoute(
|
||||
diskStatus: context
|
||||
.watch<ApiServerVolumeCubit>()
|
||||
.state
|
||||
.diskStatus,
|
||||
diskStatus:
|
||||
context.read<ApiServerVolumeCubit>().state.diskStatus,
|
||||
services: context
|
||||
.read<ServicesCubit>()
|
||||
.state
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|