diff --git a/Sora/Utils/Networking/ResolverDNS.swift b/Sora/Utils/Networking/ResolverDNS.swift deleted file mode 100644 index 4342f6c..0000000 --- a/Sora/Utils/Networking/ResolverDNS.swift +++ /dev/null @@ -1,280 +0,0 @@ -// -// ResolverDNS.swift -// Sulfur -// -// Created by seiike on 28/03/2025. -// - -import Foundation -import Network - -// MARK: - DNS Provider Enum - -enum DNSProvider: String, CaseIterable, Hashable { - case cloudflare = "Cloudflare" - case google = "Google" - case openDNS = "OpenDNS" - case quad9 = "Quad9" - case adGuard = "AdGuard" - case cleanbrowsing = "CleanBrowsing" - case controld = "ControlD" - - var servers: [String] { - switch self { - case .cloudflare: - return ["1.1.1.1", "1.0.0.1"] - case .google: - return ["8.8.8.8", "8.8.4.4"] - case .openDNS: - return ["208.67.222.222", "208.67.220.220"] - case .quad9: - return ["9.9.9.9", "149.112.112.112"] - case .adGuard: - return ["94.140.14.14", "94.140.15.15"] - case .cleanbrowsing: - return ["185.228.168.168", "185.228.169.168"] - case .controld: - return ["76.76.2.0", "76.76.10.0"] - } - } - - static var current: DNSProvider { - get { - let raw = UserDefaults.standard.string(forKey: "SelectedDNSProvider") ?? DNSProvider.cloudflare.rawValue - return DNSProvider(rawValue: raw) ?? .cloudflare - } - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "SelectedDNSProvider") - } - } -} - -// MARK: - DNS Resolver Errors - -enum DNSResolverError: Error { - case invalidResponse - case noAnswer - case connectionError(String) - case timeout -} - -// MARK: - CustomDNSResolver Class - -class CustomDNSResolver { - - /// Returns an array of DNS servers. - /// If a custom provider ("Custom") is selected in UserDefaults, it returns the custom primary and secondary values; - /// otherwise, it falls back to the default provider's servers. - var dnsServers: [String] { - if let provider = UserDefaults.standard.string(forKey: "CustomDNSProvider"), - provider == "Custom" { - let primary = UserDefaults.standard.string(forKey: "customPrimaryDNS") ?? "" - let secondary = UserDefaults.standard.string(forKey: "customSecondaryDNS") ?? "" - var servers = [String]() - if !primary.isEmpty { servers.append(primary) } - if !secondary.isEmpty { servers.append(secondary) } - if !servers.isEmpty { - return servers - } - } - return DNSProvider.current.servers - } - - /// Resolves the provided hostname by sending a DNS query over UDP. - /// - Parameters: - /// - hostname: The hostname to resolve. - /// - timeout: How long to wait for a response (default 5 seconds). - /// - completion: A closure called with the result: a list of IPv4 addresses or an error. - func resolve(hostname: String, timeout: TimeInterval = 5.0, completion: @escaping (Result<[String], Error>) -> Void) { - // Use the first DNS server from our list - guard let dnsServer = dnsServers.first else { - completion(.failure(DNSResolverError.connectionError("No DNS server available"))) - return - } - - let port: NWEndpoint.Port = 53 - let queryID = UInt16.random(in: 0...UInt16.max) - - guard let queryData = buildDNSQuery(hostname: hostname, queryID: queryID) else { - completion(.failure(DNSResolverError.connectionError("Failed to build DNS query"))) - return - } - - // Create a new UDP connection - let connection = NWConnection(host: NWEndpoint.Host(dnsServer), port: port, using: .udp) - - // Track connection state manually - var localState = NWConnection.State.setup - - connection.stateUpdateHandler = { [weak self] newState in - localState = newState - switch newState { - case .ready: - // Send the DNS query - connection.send(content: queryData, completion: .contentProcessed({ error in - if let error = error { - connection.cancel() - completion(.failure(DNSResolverError.connectionError(error.localizedDescription))) - } else { - // Receive the DNS response - self?.receiveDNSResponse(connection: connection, - expectedQueryID: queryID, - completion: completion) - } - })) - case .failed(let error): - connection.cancel() - completion(.failure(DNSResolverError.connectionError(error.localizedDescription))) - default: - break - } - } - - // Start the connection - connection.start(queue: DispatchQueue.global()) - - // Implement a timeout for the query using a switch on localState - DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { - switch localState { - case .failed(_), .cancelled: - // Already failed or canceled; do nothing - break - default: - // Not failed or canceled => consider it timed out - connection.cancel() - completion(.failure(DNSResolverError.timeout)) - } - } - } - - // MARK: - Receiving and Parsing - - private func receiveDNSResponse(connection: NWConnection, - expectedQueryID: UInt16, - completion: @escaping (Result<[String], Error>) -> Void) { - connection.receiveMessage { [weak self] data, _, _, error in - connection.cancel() - - if let error = error { - completion(.failure(DNSResolverError.connectionError(error.localizedDescription))) - return - } - guard let data = data else { - completion(.failure(DNSResolverError.invalidResponse)) - return - } - - if let ips = self?.parseDNSResponse(data: data, queryID: expectedQueryID), !ips.isEmpty { - completion(.success(ips)) - } else { - completion(.failure(DNSResolverError.noAnswer)) - } - } - } - - // MARK: - DNS Query Construction - - /// Constructs a DNS query packet for the given hostname. - /// - Parameters: - /// - hostname: The hostname to resolve. - /// - queryID: A randomly generated query identifier. - /// - Returns: A Data object representing the DNS query. - private func buildDNSQuery(hostname: String, queryID: UInt16) -> Data? { - var data = Data() - - // Header: ID (2 bytes) - data.append(contentsOf: withUnsafeBytes(of: queryID.bigEndian, Array.init)) - - // Flags: standard query with recursion desired (0x0100) - let flags: UInt16 = 0x0100 - data.append(contentsOf: withUnsafeBytes(of: flags.bigEndian, Array.init)) - - // QDCOUNT = 1 - let qdcount: UInt16 = 1 - data.append(contentsOf: withUnsafeBytes(of: qdcount.bigEndian, Array.init)) - - // ANCOUNT = 0, NSCOUNT = 0, ARCOUNT = 0 - let zero: UInt16 = 0 - data.append(contentsOf: withUnsafeBytes(of: zero.bigEndian, Array.init)) // ANCOUNT - data.append(contentsOf: withUnsafeBytes(of: zero.bigEndian, Array.init)) // NSCOUNT - data.append(contentsOf: withUnsafeBytes(of: zero.bigEndian, Array.init)) // ARCOUNT - - // Question section: - // QNAME: Encode hostname by splitting into labels. - let labels = hostname.split(separator: ".") - for label in labels { - guard let labelData = label.data(using: .utf8) else { - return nil - } - data.append(UInt8(labelData.count)) - data.append(labelData) - } - // Terminate QNAME with zero byte. - data.append(0) - - // QTYPE: A record (1) - let qtype: UInt16 = 1 - data.append(contentsOf: withUnsafeBytes(of: qtype.bigEndian, Array.init)) - - // QCLASS: IN (1) - let qclass: UInt16 = 1 - data.append(contentsOf: withUnsafeBytes(of: qclass.bigEndian, Array.init)) - - return data - } - - // MARK: - DNS Response Parsing - - /// Parses the DNS response packet and extracts IPv4 addresses from A record answers. - /// - Parameters: - /// - data: The DNS response data. - /// - queryID: The expected query identifier. - /// - Returns: An array of IPv4 address strings, or nil if parsing fails. - private func parseDNSResponse(data: Data, queryID: UInt16) -> [String]? { - // Ensure the response is at least long enough for a header. - guard data.count >= 12 else { return nil } - - // ID is the first 2 bytes - let responseID = data.subdata(in: 0..<2).withUnsafeBytes { $0.load(as: UInt16.self).bigEndian } - guard responseID == queryID else { return nil } - - // ANCOUNT is at offset 6. - let ancount = data.subdata(in: 6..<8).withUnsafeBytes { $0.load(as: UInt16.self).bigEndian } - if ancount == 0 { return nil } - - // Skip the header and question section. - var offset = 12 - // Skip QNAME - while offset < data.count && data[offset] != 0 { - offset += Int(data[offset]) + 1 - } - offset += 1 // Skip the terminating zero. - - // Skip QTYPE (2 bytes) and QCLASS (2 bytes) - offset += 4 - - var ips: [String] = [] - - // Loop through answer records. - for _ in 0.. data.count { break } - offset += 2 // Skip NAME (pointer) - let type = data.subdata(in: offset..<(offset+2)).withUnsafeBytes { $0.load(as: UInt16.self).bigEndian } - offset += 2 // TYPE - offset += 2 // CLASS - offset += 4 // TTL - let rdlength = data.subdata(in: offset..<(offset+2)).withUnsafeBytes { $0.load(as: UInt16.self).bigEndian } - offset += 2 - - // If the record is an A record and the length is 4 bytes, extract the IPv4 address. - if type == 1 && rdlength == 4 && offset + 4 <= data.count { - let ipBytes = data.subdata(in: offset..<(offset+4)) - let ip = ipBytes.map { String($0) }.joined(separator: ".") - ips.append(ip) - } - offset += Int(rdlength) - } - - return ips - } -} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 66fe45f..ad75420 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; - 1E2719D92D975126008C4BD0 /* ResolverDNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2719D82D975120008C4BD0 /* ResolverDNS.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; @@ -118,7 +117,6 @@ 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; - 1E2719D82D975120008C4BD0 /* ResolverDNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolverDNS.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; @@ -286,7 +284,6 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( - 1E2719D72D9750FB008C4BD0 /* Networking */, 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, @@ -440,14 +437,6 @@ path = Components; sourceTree = ""; }; - 1E2719D72D9750FB008C4BD0 /* Networking */ = { - isa = PBXGroup; - children = ( - 1E2719D82D975120008C4BD0 /* ResolverDNS.swift */, - ); - path = Networking; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -535,7 +524,6 @@ 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */, 139935662D468C450065CEFF /* ModuleManager.swift in Sources */, 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, - 1E2719D92D975126008C4BD0 /* ResolverDNS.swift in Sources */, 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, 13DB46902D900A38008CBC03 /* URL.swift in Sources */,