From 2640b830e1edb6ed0dce8a30e6797cae1057b28f Mon Sep 17 00:00:00 2001 From: Jackie264 <125248570+Jackie264@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:47:38 +0800 Subject: [PATCH 1/6] Create dns_query_service.dart --- mobile/lib/services/dns_query_service.dart | 1 + 1 file changed, 1 insertion(+) create mode 100644 mobile/lib/services/dns_query_service.dart diff --git a/mobile/lib/services/dns_query_service.dart b/mobile/lib/services/dns_query_service.dart new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/mobile/lib/services/dns_query_service.dart @@ -0,0 +1 @@ + From 90f9b08cb34650acd3891df7bcf9a0e5aa27f81a Mon Sep 17 00:00:00 2001 From: Jackie264 <125248570+Jackie264@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:50:07 +0800 Subject: [PATCH 2/6] Create dns_query_service.dart DNS Query Service --- mobile/lib/services/dns_query_service.dart | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/mobile/lib/services/dns_query_service.dart b/mobile/lib/services/dns_query_service.dart index 8b137891791fe..614e6514e0eca 100644 --- a/mobile/lib/services/dns_query_service.dart +++ b/mobile/lib/services/dns_query_service.dart @@ -1 +1,106 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +class DnsQueryService { + final String dnsServer; + + /// Constructor with optional custom DNS server address. + DnsQueryService({this.dnsServer = '8.8.8.8'}); + + /// Query DNS for HTTPS or SRV records and return the resolved URL. + Future resolveDns(String domain) async { + final httpsRecord = await _queryDns(domain, 'HTTPS'); + if (httpsRecord != null) { + return httpsRecord; + } + + final srvRecord = await _queryDns(domain, 'SRV'); + if (srvRecord != null) { + return srvRecord; + } + + return null; + } + + /// Internal method to query DNS for specific record types. + Future _queryDns(String domain, String recordType) async { + final queryType = recordType == 'HTTPS' ? 65 : 33; // 65: HTTPS, 33: SRV + RawDatagramSocket? rawSocket; + try { + final bindAddress = InternetAddress.anyIPv6; + rawSocket = await RawDatagramSocket.bind(bindAddress, 0); + + final dnsRequest = _buildDnsRequest(domain, queryType); + final targetAddress = InternetAddress(dnsServer, type: InternetAddressType.any); + rawSocket.send(dnsRequest, targetAddress, 53); + + await for (var event in rawSocket) { + if (event == RawSocketEvent.read) { + final response = rawSocket.receive(); + if (response != null) { + return _parseDnsResponse(response.data, recordType); + } + } + } + } on SocketException catch (e) { + print('IPv6 failed, falling back to IPv4: $e'); + try { + rawSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); + final dnsRequest = _buildDnsRequest(domain, queryType); + final targetAddress = InternetAddress(dnsServer, type: InternetAddressType.any); + rawSocket.send(dnsRequest, targetAddress, 53); + + await for (var event in rawSocket) { + if (event == RawSocketEvent.read) { + final response = rawSocket.receive(); + if (response != null) { + return _parseDnsResponse(response.data, recordType); + } + } + } + } catch (fallbackError) { + print('IPv4 fallback also failed: $fallbackError'); + } + } catch (e) { + print('DNS query failed: $e'); + } finally { + rawSocket?.close(); + } + + return null; + } + + /// Build a DNS request packet for the given domain and record type. + List _buildDnsRequest(String domain, int queryType) { + final packet = []; + + // Random transaction ID + final transactionId = List.generate(2, (_) => Random().nextInt(256)); + packet.addAll(transactionId); + + packet.addAll([0x01, 0x00]); // Flags + packet.addAll([0x00, 0x01]); // Questions + packet.addAll([0x00, 0x00, 0x00, 0x00]); // Answer/Authority/Additional RRs + + for (final part in domain.split('.')) { + packet.add(part.length); + packet.addAll(part.codeUnits); + } + packet.add(0); // End of domain name + packet.addAll([0x00, queryType]); // Query type + packet.addAll([0x00, 0x01]); // Query class (IN) + return packet; + } + + /// Parse the DNS response and extract the target domain and port if applicable. + String? _parseDnsResponse(List response, String recordType) { + // Simplified parsing logic + if (recordType == 'HTTPS') { + // Implement HTTPS record parsing + } else if (recordType == 'SRV') { + // Implement SRV record parsing + } + return null; + } +} From b3a9e4db23122a134c74d670a9547825172f36c8 Mon Sep 17 00:00:00 2001 From: Jackie264 <125248570+Jackie264@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:53:13 +0800 Subject: [PATCH 3/6] Update api.service.dart Resolves the server endpoint and sets the base path for the API client. If DNS resolution fails, it falls back to the original server URL. Logs the resolution process for debugging. --- mobile/lib/services/api.service.dart | 48 +++++++++++++++++++++------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 0f6fe8a100ef8..4b0f18c10d204 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -10,6 +10,8 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:http/http.dart'; +import 'dns_query_service.dart'; + class ApiService implements Authentication { late ApiClient _apiClient; @@ -83,6 +85,9 @@ class ApiService implements Authentication { /// port - optional (default: based on schema) /// path - optional Future resolveEndpoint(String serverUrl) async { + if (!Uri.tryParse(serverUrl)?.hasAbsolutePath ?? false) { + throw ApiException(400, "Invalid server URL: $serverUrl"); + } final url = sanitizeUrl(serverUrl); if (!await _isEndpointAvailable(serverUrl)) { @@ -97,7 +102,23 @@ class ApiService implements Authentication { return url; } + final DnsQueryService _dnsQueryService = DnsQueryService(); + + /// Resolves the server endpoint and sets the base path for the API client. + /// + /// If DNS resolution fails, it falls back to the original server URL. + /// Logs the resolution process for debugging. Future _isEndpointAvailable(String serverUrl) async { + final parsedUri = Uri.parse(serverUrl); + final domain = parsedUri.host; + + final resolvedUrl = await _dnsQueryService.resolveDns(domain); + if (resolvedUrl != null) { + serverUrl = resolvedUrl; + } else { + _log.info("DNS resolution failed for domain: $domain, original serverUrl: $serverUrl"); + } + if (!serverUrl.endsWith('/api')) { serverUrl += '/api'; } @@ -117,6 +138,7 @@ class ApiService implements Authentication { ); return false; } + _log.info('Resolved URL: $resolvedUrl, Final server URL: $serverUrl'); return true; } @@ -143,7 +165,7 @@ class ApiService implements Authentication { return endpoint; } } catch (e) { - debugPrint("Could not locate /.well-known/immich at $baseUrl"); + _log.warning("Could not locate /.well-known/immich at $baseUrl"); } return ""; @@ -157,16 +179,20 @@ class ApiService implements Authentication { Future setDeviceInfoHeader() async { DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - if (Platform.isIOS) { - final iosInfo = await deviceInfoPlugin.iosInfo; - authenticationApi.apiClient - .addDefaultHeader('deviceModel', iosInfo.utsname.machine); - authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS'); - } else { - final androidInfo = await deviceInfoPlugin.androidInfo; - authenticationApi.apiClient - .addDefaultHeader('deviceModel', androidInfo.model); - authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android'); + try { + if (Platform.isIOS) { + final iosInfo = await deviceInfoPlugin.iosInfo; + authenticationApi.apiClient + .addDefaultHeader('deviceModel', iosInfo.utsname.machine); + authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS'); + } else { + final androidInfo = await deviceInfoPlugin.androidInfo; + authenticationApi.apiClient + .addDefaultHeader('deviceModel', androidInfo.model); + authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android'); + } + } catch (e) { + _log.warning('Failed to set device info headers: $e'); } } From d2c31bb3c7453c48738ac31639005a4b9db58105 Mon Sep 17 00:00:00 2001 From: Jackie264 <125248570+Jackie264@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:59:17 +0800 Subject: [PATCH 4/6] Update api.service.dart --- mobile/lib/services/api.service.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 4b0f18c10d204..09fa04b24ee0b 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -10,6 +10,7 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:http/http.dart'; +/// Create dns_query_service.dart import 'dns_query_service.dart'; class ApiService implements Authentication { @@ -85,6 +86,7 @@ class ApiService implements Authentication { /// port - optional (default: based on schema) /// path - optional Future resolveEndpoint(String serverUrl) async { + // Check for server URL if (!Uri.tryParse(serverUrl)?.hasAbsolutePath ?? false) { throw ApiException(400, "Invalid server URL: $serverUrl"); } From a369faae9005189bc66c27cad1c7b4a2804ef630 Mon Sep 17 00:00:00 2001 From: Jackie264 <125248570+Jackie264@users.noreply.github.com> Date: Thu, 2 Jan 2025 12:55:27 +0800 Subject: [PATCH 5/6] Use Dart-Basic-Utils for SRV record lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR replaces the manual DNS query implementation with the Dart-Basic-Utils library. Key reasons for the change: • Avoids maintaining a custom DNS query logic. • Leverages a reliable third-party library that supports various DNS record types (e.g., SRV). • Simplifies the codebase and improves maintainability. --- mobile/lib/services/dns_query_service.dart | 141 ++++++--------------- 1 file changed, 41 insertions(+), 100 deletions(-) diff --git a/mobile/lib/services/dns_query_service.dart b/mobile/lib/services/dns_query_service.dart index 614e6514e0eca..ec68dd9b2375f 100644 --- a/mobile/lib/services/dns_query_service.dart +++ b/mobile/lib/services/dns_query_service.dart @@ -1,106 +1,47 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:math'; +import 'package:basic_utils/basic_utils.dart'; +import 'package:logging/logging.dart'; class DnsQueryService { - final String dnsServer; + final Logger _log = Logger('DnsQueryService'); - /// Constructor with optional custom DNS server address. - DnsQueryService({this.dnsServer = '8.8.8.8'}); - - /// Query DNS for HTTPS or SRV records and return the resolved URL. - Future resolveDns(String domain) async { - final httpsRecord = await _queryDns(domain, 'HTTPS'); - if (httpsRecord != null) { - return httpsRecord; - } - - final srvRecord = await _queryDns(domain, 'SRV'); - if (srvRecord != null) { - return srvRecord; - } - - return null; - } - - /// Internal method to query DNS for specific record types. - Future _queryDns(String domain, String recordType) async { - final queryType = recordType == 'HTTPS' ? 65 : 33; // 65: HTTPS, 33: SRV - RawDatagramSocket? rawSocket; - try { - final bindAddress = InternetAddress.anyIPv6; - rawSocket = await RawDatagramSocket.bind(bindAddress, 0); - - final dnsRequest = _buildDnsRequest(domain, queryType); - final targetAddress = InternetAddress(dnsServer, type: InternetAddressType.any); - rawSocket.send(dnsRequest, targetAddress, 53); - - await for (var event in rawSocket) { - if (event == RawSocketEvent.read) { - final response = rawSocket.receive(); - if (response != null) { - return _parseDnsResponse(response.data, recordType); - } - } - } - } on SocketException catch (e) { - print('IPv6 failed, falling back to IPv4: $e'); - try { - rawSocket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); - final dnsRequest = _buildDnsRequest(domain, queryType); - final targetAddress = InternetAddress(dnsServer, type: InternetAddressType.any); - rawSocket.send(dnsRequest, targetAddress, 53); - - await for (var event in rawSocket) { - if (event == RawSocketEvent.read) { - final response = rawSocket.receive(); - if (response != null) { - return _parseDnsResponse(response.data, recordType); - } - } - } - } catch (fallbackError) { - print('IPv4 fallback also failed: $fallbackError'); - } - } catch (e) { - print('DNS query failed: $e'); - } finally { - rawSocket?.close(); - } - - return null; - } - - /// Build a DNS request packet for the given domain and record type. - List _buildDnsRequest(String domain, int queryType) { - final packet = []; - - // Random transaction ID - final transactionId = List.generate(2, (_) => Random().nextInt(256)); - packet.addAll(transactionId); - - packet.addAll([0x01, 0x00]); // Flags - packet.addAll([0x00, 0x01]); // Questions - packet.addAll([0x00, 0x00, 0x00, 0x00]); // Answer/Authority/Additional RRs - - for (final part in domain.split('.')) { - packet.add(part.length); - packet.addAll(part.codeUnits); - } - packet.add(0); // End of domain name - packet.addAll([0x00, queryType]); // Query type - packet.addAll([0x00, 0x01]); // Query class (IN) - return packet; - } - - /// Parse the DNS response and extract the target domain and port if applicable. - String? _parseDnsResponse(List response, String recordType) { - // Simplified parsing logic - if (recordType == 'HTTPS') { - // Implement HTTPS record parsing - } else if (recordType == 'SRV') { - // Implement SRV record parsing - } - return null; + /// Validates the domain name format. + bool _isValidDomain(String domain) { + final regex = RegExp(r'^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$'); + return regex.hasMatch(domain); } + + /// Fetch SRV records for a given domain. + Future>> fetchSrvRecords(String domain) async { + if (!_isValidDomain(domain)) { + _log.warning('Invalid domain name: $domain'); + throw Exception('Invalid domain name: $domain'); + } + + try { + _log.info('Attempting to fetch SRV records for domain: $domain'); + + // Perform the SRV record lookup + final srvRecords = await DnsUtils.lookupRecord(domain, RRecordType.SRV); + + _log.info('Successfully fetched SRV records for domain: $domain'); + + // Transform the result into a list of maps + return srvRecords.map((record) { + return { + 'priority': record.priority, + 'weight': record.weight, + 'port': record.port, + 'target': record.target, + }; + }).toList(); + } catch (e, stackTrace) { + _log.severe( + 'Failed to fetch SRV records for domain: $domain', + e, + stackTrace, + ); + throw Exception('Failed to fetch SRV records: $e'); + } + } } From d28aeee8e1872cb10981fc016bdd5a51a144537b Mon Sep 17 00:00:00 2001 From: Jackie264 <125248570+Jackie264@users.noreply.github.com> Date: Thu, 2 Jan 2025 13:24:03 +0800 Subject: [PATCH 6/6] Update api.service.dart import dns_query_service.dart; Create DnsQueryService instance; Resolve and set the API endpoint, with SRV record lookup based on domain --- mobile/lib/services/api.service.dart | 66 +++++++++++----------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 09fa04b24ee0b..aa3110b25bf0b 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -10,12 +10,10 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:http/http.dart'; -/// Create dns_query_service.dart -import 'dns_query_service.dart'; +import 'package:dns_query_service.dart'; class ApiService implements Authentication { late ApiClient _apiClient; - late UsersApi usersApi; late AuthenticationApi authenticationApi; late OAuthApi oAuthApi; @@ -34,7 +32,8 @@ class ApiService implements Authentication { late DownloadApi downloadApi; late TrashApi trashApi; late StacksApi stacksApi; - + final DnsQueryService _dnsQueryService = DnsQueryService(); // Create DnsQueryService instance + ApiService() { final endpoint = Store.tryGet(StoreKey.serverEndpoint); if (endpoint != null && endpoint.isNotEmpty) { @@ -85,17 +84,25 @@ class ApiService implements Authentication { /// host - required /// port - optional (default: based on schema) /// path - optional + /// Resolve and set the API endpoint, with SRV record lookup based on domain Future resolveEndpoint(String serverUrl) async { - // Check for server URL - if (!Uri.tryParse(serverUrl)?.hasAbsolutePath ?? false) { - throw ApiException(400, "Invalid server URL: $serverUrl"); - } final url = sanitizeUrl(serverUrl); if (!await _isEndpointAvailable(serverUrl)) { throw ApiException(503, "Server is not reachable"); } + // Get domain name from serverUrl + final domain = Uri.parse(serverUrl).host; + + // Call DnsQueryService to fetch SRV records + try { + List> srvRecords = await _dnsQueryService.fetchSrvRecords(domain); + _log.info('Fetched SRV records for domain $domain: $srvRecords'); + } catch (e) { + _log.severe('Failed to fetch SRV records for domain $domain: $e'); + } + // Check for /.well-known/immich final wellKnownEndpoint = await _getWellKnownEndpoint(url); if (wellKnownEndpoint.isNotEmpty) return wellKnownEndpoint; @@ -104,23 +111,7 @@ class ApiService implements Authentication { return url; } - final DnsQueryService _dnsQueryService = DnsQueryService(); - - /// Resolves the server endpoint and sets the base path for the API client. - /// - /// If DNS resolution fails, it falls back to the original server URL. - /// Logs the resolution process for debugging. Future _isEndpointAvailable(String serverUrl) async { - final parsedUri = Uri.parse(serverUrl); - final domain = parsedUri.host; - - final resolvedUrl = await _dnsQueryService.resolveDns(domain); - if (resolvedUrl != null) { - serverUrl = resolvedUrl; - } else { - _log.info("DNS resolution failed for domain: $domain, original serverUrl: $serverUrl"); - } - if (!serverUrl.endsWith('/api')) { serverUrl += '/api'; } @@ -140,7 +131,6 @@ class ApiService implements Authentication { ); return false; } - _log.info('Resolved URL: $resolvedUrl, Final server URL: $serverUrl'); return true; } @@ -167,7 +157,7 @@ class ApiService implements Authentication { return endpoint; } } catch (e) { - _log.warning("Could not locate /.well-known/immich at $baseUrl"); + debugPrint("Could not locate /.well-known/immich at $baseUrl"); } return ""; @@ -181,20 +171,16 @@ class ApiService implements Authentication { Future setDeviceInfoHeader() async { DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - try { - if (Platform.isIOS) { - final iosInfo = await deviceInfoPlugin.iosInfo; - authenticationApi.apiClient - .addDefaultHeader('deviceModel', iosInfo.utsname.machine); - authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS'); - } else { - final androidInfo = await deviceInfoPlugin.androidInfo; - authenticationApi.apiClient - .addDefaultHeader('deviceModel', androidInfo.model); - authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android'); - } - } catch (e) { - _log.warning('Failed to set device info headers: $e'); + if (Platform.isIOS) { + final iosInfo = await deviceInfoPlugin.iosInfo; + authenticationApi.apiClient + .addDefaultHeader('deviceModel', iosInfo.utsname.machine); + authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS'); + } else { + final androidInfo = await deviceInfoPlugin.androidInfo; + authenticationApi.apiClient + .addDefaultHeader('deviceModel', androidInfo.model); + authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android'); } }