import 'dart:io'; import 'package:basic_utils/basic_utils.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_creator.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_factory.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/server_provider_factory.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/logic/models/json/device_token.dart'; import 'package:selfprivacy/logic/models/message.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; class IpNotFoundException implements Exception { IpNotFoundException(this.message); final String message; } class ServerAuthorizationException implements Exception { ServerAuthorizationException(this.message); final String message; } class ServerInstallationRepository { Box box = Hive.box(BNames.serverInstallationBox); Box usersBox = Hive.box(BNames.usersBox); ServerProviderApiFactory? serverProviderApiFactory = ApiFactoryCreator.createServerProviderApiFactory( ServerProvider.hetzner, // TODO: HARDCODE FOR NOW!!! ); // TODO: Remove when provider selection is implemented. DnsProviderApiFactory? dnsProviderApiFactory = ApiFactoryCreator.createDnsProviderApiFactory( DnsProvider.cloudflare, // TODO: HARDCODE FOR NOW!!! ); Future load() async { final String? providerApiToken = getIt().hetznerKey; final String? cloudflareToken = getIt().cloudFlareKey; final ServerDomain? serverDomain = getIt().serverDomain; final BackblazeCredential? backblazeCredential = getIt().backblazeCredential; final ServerHostingDetails? serverDetails = getIt().serverDetails; if (serverDetails != null && serverDetails.provider != ServerProvider.unknown) { serverProviderApiFactory = ApiFactoryCreator.createServerProviderApiFactory( serverDetails.provider, ); } if (serverDomain != null && serverDomain.provider != DnsProvider.unknown) { dnsProviderApiFactory = ApiFactoryCreator.createDnsProviderApiFactory( serverDomain.provider, ); } if (box.get(BNames.hasFinalChecked, defaultValue: false)) { return ServerInstallationFinished( providerApiToken: providerApiToken!, cloudFlareKey: cloudflareToken!, serverDomain: serverDomain!, backblazeCredential: backblazeCredential!, serverDetails: serverDetails!, rootUser: box.get(BNames.rootUser), isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), isServerResetedFirstTime: box.get(BNames.isServerResetedFirstTime, defaultValue: false), isServerResetedSecondTime: box.get(BNames.isServerResetedSecondTime, defaultValue: false), ); } if (box.get(BNames.isRecoveringServer, defaultValue: false) && serverDomain != null) { return ServerInstallationRecovery( providerApiToken: providerApiToken, cloudFlareKey: cloudflareToken, serverDomain: serverDomain, backblazeCredential: backblazeCredential, serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), currentStep: _getCurrentRecoveryStep( providerApiToken, cloudflareToken, serverDomain, serverDetails, ), recoveryCapabilities: await getRecoveryCapabilities(serverDomain), ); } return ServerInstallationNotFinished( providerApiToken: providerApiToken, cloudFlareKey: cloudflareToken, serverDomain: serverDomain, backblazeCredential: backblazeCredential, serverDetails: serverDetails, rootUser: box.get(BNames.rootUser), isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), isServerResetedFirstTime: box.get(BNames.isServerResetedFirstTime, defaultValue: false), isServerResetedSecondTime: box.get(BNames.isServerResetedSecondTime, defaultValue: false), isLoading: box.get(BNames.isLoading, defaultValue: false), dnsMatches: null, ); } RecoveryStep _getCurrentRecoveryStep( final String? hetznerToken, final String? cloudflareToken, final ServerDomain serverDomain, final ServerHostingDetails? serverDetails, ) { if (serverDetails != null) { if (hetznerToken != null) { if (serverDetails.provider != ServerProvider.unknown) { if (serverDomain.provider != DnsProvider.unknown) { return RecoveryStep.backblazeToken; } return RecoveryStep.cloudflareToken; } return RecoveryStep.serverSelection; } return RecoveryStep.hetznerToken; } return RecoveryStep.selecting; } void clearAppConfig() { box.clear(); usersBox.clear(); } Future startServer( final ServerHostingDetails hetznerServer, ) async { ServerHostingDetails? serverDetails; final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); serverDetails = await api.powerOn(); return serverDetails; } Future getDomainId(final String token, final String domain) async { final DnsProviderApi dnsProviderApi = dnsProviderApiFactory!.getDnsProvider( settings: DnsProviderApiSettings( isWithToken: false, customToken: token, ), ); try { final String domainId = await dnsProviderApi.getZoneId(domain); return domainId; } on DomainNotFoundException { return null; } } Future> isDnsAddressesMatch( final String? domainName, final String? ip4, final Map skippedMatches, ) async { final List addresses = [ '$domainName', 'api.$domainName', 'cloud.$domainName', 'meet.$domainName', 'password.$domainName' ]; final Map matches = {}; for (final String address in addresses) { if (skippedMatches[address] ?? false) { matches[address] = true; continue; } final List? lookupRecordRes = await DnsUtils.lookupRecord( address, RRecordType.A, provider: DnsApiProvider.CLOUDFLARE, ); getIt.get().addMessage( Message( text: 'DnsLookup: address: $address, $RRecordType, provider: CLOUDFLARE, ip4: $ip4', ), ); getIt.get().addMessage( Message( text: 'DnsLookup: ${lookupRecordRes == null ? 'empty' : (lookupRecordRes[0].data != ip4 ? 'wrong ip4' : 'right ip4')}', ), ); if (lookupRecordRes == null || lookupRecordRes.isEmpty || lookupRecordRes[0].data != ip4) { matches[address] = false; } else { matches[address] = true; } } return matches; } Future createServer( final User rootUser, final String domainName, final String cloudFlareKey, final BackblazeCredential backblazeCredential, { required final void Function() onCancel, required final Future Function(ServerHostingDetails serverDetails) onSuccess, }) async { final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); try { final ServerHostingDetails? serverDetails = await api.createServer( dnsApiToken: cloudFlareKey, rootUser: rootUser, domainName: domainName, ); if (serverDetails == null) { print('Server is not initialized!'); return; } saveServerDetails(serverDetails); onSuccess(serverDetails); } on DioError catch (e) { if (e.response!.data['error']['code'] == 'uniqueness_error') { final NavigationService nav = getIt.get(); nav.showPopUpDialog( BrandAlert( title: 'modals.1'.tr(), contentText: 'modals.2'.tr(), actions: [ ActionButton( text: 'basis.delete'.tr(), isRed: true, onPressed: () async { await api.deleteServer( domainName: domainName, ); ServerHostingDetails? serverDetails; try { serverDetails = await api.createServer( dnsApiToken: cloudFlareKey, rootUser: rootUser, domainName: domainName, ); } catch (e) { print(e); } if (serverDetails == null) { print('Server is not initialized!'); return; } await saveServerDetails(serverDetails); onSuccess(serverDetails); }, ), ActionButton( text: 'basis.cancel'.tr(), onPressed: onCancel, ), ], ), ); } } } Future createDnsRecords( final ServerHostingDetails serverDetails, final ServerDomain domain, { required final void Function() onCancel, }) async { final DnsProviderApi dnsProviderApi = dnsProviderApiFactory!.getDnsProvider(); final ServerProviderApi serverApi = serverProviderApiFactory!.getServerProvider(); await dnsProviderApi.removeSimilarRecords( ip4: serverDetails.ip4, domain: domain, ); try { await dnsProviderApi.createMultipleDnsRecords( ip4: serverDetails.ip4, domain: domain, ); } on DioError catch (e) { final NavigationService nav = getIt.get(); nav.showPopUpDialog( BrandAlert( title: e.response!.data['errors'][0]['code'] == 1038 ? 'modals.10'.tr() : 'providers.domain.states.error'.tr(), contentText: 'modals.6'.tr(), actions: [ ActionButton( text: 'basis.delete'.tr(), isRed: true, onPressed: () async { await serverApi.deleteServer( domainName: domain.domainName, ); onCancel(); }, ), ActionButton( text: 'basis.cancel'.tr(), onPressed: onCancel, ), ], ), ); return false; } await serverApi.createReverseDns( serverDetails: serverDetails, domain: domain, ); return true; } Future createDkimRecord(final ServerDomain cloudFlareDomain) async { final DnsProviderApi dnsProviderApi = dnsProviderApiFactory!.getDnsProvider(); final ServerApi api = ServerApi(); final String? dkimRecordString = await api.getDkim(); await dnsProviderApi.setDkim(dkimRecordString ?? '', cloudFlareDomain); } Future isHttpServerWorking() async { final ServerApi api = ServerApi(); final bool isHttpServerWorking = await api.isHttpServerWorking(); try { await api.getDkim(); } catch (e) { return false; } return isHttpServerWorking; } Future restart() async { final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); return api.restart(); } Future powerOn() async { final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); return api.powerOn(); } Future getRecoveryCapabilities( final ServerDomain serverDomain, ) async { final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); final String? serverApiVersion = await serverApi.getApiVersion(); if (serverApiVersion == null) { return ServerRecoveryCapabilities.none; } try { final Version parsedVersion = Version.parse(serverApiVersion); if (!VersionConstraint.parse('>=1.2.0').allows(parsedVersion)) { return ServerRecoveryCapabilities.legacy; } return ServerRecoveryCapabilities.loginTokens; } on FormatException { return ServerRecoveryCapabilities.none; } } Future getServerIpFromDomain(final ServerDomain serverDomain) async { final List? lookup = await DnsUtils.lookupRecord( serverDomain.domainName, RRecordType.A, provider: DnsApiProvider.CLOUDFLARE, ); if (lookup == null || lookup.isEmpty) { throw IpNotFoundException('No IP found for domain $serverDomain'); } return lookup[0].data; } Future getDeviceName() async { final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (kIsWeb) { return deviceInfo.webBrowserInfo.then( (final WebBrowserInfo value) => '${value.browserName} ${value.platform}', ); } else { if (Platform.isAndroid) { return deviceInfo.androidInfo.then( (final AndroidDeviceInfo value) => '${value.model} ${value.version.release}', ); } else if (Platform.isIOS) { return deviceInfo.iosInfo.then( (final IosDeviceInfo value) => '${value.utsname.machine} ${value.systemName} ${value.systemVersion}', ); } else if (Platform.isLinux) { return deviceInfo.linuxInfo .then((final LinuxDeviceInfo value) => value.prettyName); } else if (Platform.isMacOS) { return deviceInfo.macOsInfo.then( (final MacOsDeviceInfo value) => '${value.hostName} ${value.computerName}', ); } else if (Platform.isWindows) { return deviceInfo.windowsInfo .then((final WindowsDeviceInfo value) => value.computerName); } } return 'Unidentified'; } Future authorizeByNewDeviceKey( final ServerDomain serverDomain, final String newDeviceKey, final ServerRecoveryCapabilities recoveryCapabilities, ) async { final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); final String serverIp = await getServerIpFromDomain(serverDomain); final ApiResponse apiResponse = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: newDeviceKey), ); if (apiResponse.isSuccess) { return ServerHostingDetails( apiToken: apiResponse.data, volume: ServerVolume( id: 0, name: '', sizeByte: 0, serverId: 0, ), provider: ServerProvider.unknown, id: 0, ip4: serverIp, startTime: null, createTime: null, ); } throw ServerAuthorizationException( apiResponse.errorMessage ?? apiResponse.data, ); } Future authorizeByRecoveryKey( final ServerDomain serverDomain, final String recoveryKey, final ServerRecoveryCapabilities recoveryCapabilities, ) async { final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, ); final String serverIp = await getServerIpFromDomain(serverDomain); final ApiResponse apiResponse = await serverApi.useRecoveryToken( DeviceToken(device: await getDeviceName(), token: recoveryKey), ); if (apiResponse.isSuccess) { return ServerHostingDetails( apiToken: apiResponse.data, volume: ServerVolume( id: 0, name: '', sizeByte: 0, serverId: 0, ), provider: ServerProvider.unknown, id: 0, ip4: serverIp, startTime: null, createTime: null, ); } throw ServerAuthorizationException( apiResponse.errorMessage ?? apiResponse.data, ); } Future authorizeByApiToken( final ServerDomain serverDomain, final String apiToken, final ServerRecoveryCapabilities recoveryCapabilities, ) async { final ServerApi serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, customToken: apiToken, ); final String serverIp = await getServerIpFromDomain(serverDomain); if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { final Map apiResponse = await serverApi.servicesPowerCheck(); if (apiResponse.isNotEmpty) { return ServerHostingDetails( apiToken: apiToken, volume: ServerVolume( id: 0, name: '', serverId: 0, sizeByte: 0, ), provider: ServerProvider.unknown, id: 0, ip4: serverIp, startTime: null, createTime: null, ); } else { throw ServerAuthorizationException( "Couldn't connect to server with this token", ); } } final ApiResponse deviceAuthKey = await serverApi.createDeviceToken(); final ApiResponse apiResponse = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), ); if (apiResponse.isSuccess) { return ServerHostingDetails( apiToken: apiResponse.data, volume: ServerVolume( id: 0, name: '', sizeByte: 0, serverId: 0, ), provider: ServerProvider.unknown, id: 0, ip4: serverIp, startTime: null, createTime: null, ); } throw ServerAuthorizationException( apiResponse.errorMessage ?? apiResponse.data, ); } Future getMainUser() async { final ServerApi serverApi = ServerApi(); const User fallbackUser = User( isFoundOnServer: false, note: "Couldn't find main user on server, API is outdated", login: 'UNKNOWN', sshKeys: [], ); final String? serverApiVersion = await serverApi.getApiVersion(); final ApiResponse> users = await serverApi.getUsersList(withMainUser: true); if (serverApiVersion == null || !users.isSuccess) { return fallbackUser; } try { final Version parsedVersion = Version.parse(serverApiVersion); if (!VersionConstraint.parse('>=1.2.5').allows(parsedVersion)) { return fallbackUser; } return User( isFoundOnServer: true, login: users.data[0], ); } on FormatException { return fallbackUser; } } Future> getServersOnProviderAccount() async { final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); return api.getServers(); } Future saveServerDetails( final ServerHostingDetails serverDetails, ) async { await getIt().storeServerDetails(serverDetails); } Future deleteServerDetails() async { await box.delete(BNames.serverDetails); getIt().init(); } Future saveHetznerKey(final String key) async { print('saved'); await getIt().storeHetznerKey(key); } Future deleteHetznerKey() async { await box.delete(BNames.hetznerKey); getIt().init(); } Future saveBackblazeKey( final BackblazeCredential backblazeCredential, ) async { await getIt().storeBackblazeCredential(backblazeCredential); } Future deleteBackblazeKey() async { await box.delete(BNames.backblazeCredential); getIt().init(); } Future saveCloudFlareKey(final String key) async { await getIt().storeCloudFlareKey(key); } Future deleteCloudFlareKey() async { await box.delete(BNames.cloudFlareKey); getIt().init(); } Future saveDomain(final ServerDomain serverDomain) async { await getIt().storeServerDomain(serverDomain); } Future deleteDomain() async { await box.delete(BNames.serverDomain); getIt().init(); } Future saveIsServerStarted(final bool value) async { await box.put(BNames.isServerStarted, value); } Future saveIsServerResetedFirstTime(final bool value) async { await box.put(BNames.isServerResetedFirstTime, value); } Future saveIsServerResetedSecondTime(final bool value) async { await box.put(BNames.isServerResetedSecondTime, value); } Future saveRootUser(final User rootUser) async { await box.put(BNames.rootUser, rootUser); } Future saveIsRecoveringServer(final bool value) async { await box.put(BNames.isRecoveringServer, value); } Future saveHasFinalChecked(final bool value) async { await box.put(BNames.hasFinalChecked, value); } Future deleteServer(final ServerDomain serverDomain) async { final ServerProviderApi api = serverProviderApiFactory!.getServerProvider(); final DnsProviderApi dnsProviderApi = dnsProviderApiFactory!.getDnsProvider(); await api.deleteServer( domainName: serverDomain.domainName, ); await box.put(BNames.hasFinalChecked, false); await box.put(BNames.isServerStarted, false); await box.put(BNames.isServerResetedFirstTime, false); await box.put(BNames.isServerResetedSecondTime, false); await box.put(BNames.isLoading, false); await box.put(BNames.serverDetails, null); await dnsProviderApi.removeSimilarRecords(domain: serverDomain); } Future deleteServerRelatedRecords() async { await box.deleteAll([ BNames.serverDetails, BNames.isServerStarted, BNames.isServerResetedFirstTime, BNames.isServerResetedSecondTime, BNames.hasFinalChecked, BNames.isLoading, ]); getIt().init(); } }