├── .github └── workflows │ └── ci.yml ├── .gitignore ├── License ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── Resolver │ ├── Resolver.swift │ └── SafeDict.swift └── Tests └── ResolverTests └── ResolverTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | macOS: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Test target 11 | run: swift test -v 12 | iOS: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Test target 17 | run: > 18 | swift test -v 19 | -Xswiftc "-sdk" 20 | -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" 21 | -Xswiftc "-target" -Xswiftc "x86_64-apple-ios13-simulator" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift,objective-c,xcode,carthage,osx 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xcuserstate 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | 71 | 72 | ### Objective-C ### 73 | # Xcode 74 | # 75 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 76 | 77 | ## Build generated 78 | 79 | ## Various settings 80 | 81 | ## Other 82 | 83 | ## Obj-C/Swift specific 84 | 85 | # CocoaPods 86 | # 87 | # We recommend against adding the Pods directory to your .gitignore. However 88 | # you should judge for yourself, the pros and cons are mentioned at: 89 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 90 | # 91 | # Pods/ 92 | 93 | # Carthage 94 | # 95 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 96 | # Carthage/Checkouts 97 | 98 | 99 | # fastlane 100 | # 101 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 102 | # screenshots whenever they are needed. 103 | # For more information about the recommended setup visit: 104 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 105 | 106 | 107 | # Code Injection 108 | # 109 | # After new code Injection tools there's a generated folder /iOSInjectionProject 110 | # https://github.com/johnno1962/injectionforxcode 111 | 112 | iOSInjectionProject/ 113 | 114 | ### Objective-C Patch ### 115 | *.xcscmblueprint 116 | 117 | 118 | ### Xcode ### 119 | # Xcode 120 | # 121 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 122 | 123 | ## Build generated 124 | 125 | ## Various settings 126 | 127 | ## Other 128 | *.xccheckout 129 | 130 | 131 | ### Carthage ### 132 | # Carthage - A simple, decentralized dependency manager for Cocoa 133 | Carthage/Checkouts/ 134 | Carthage/Build/ 135 | 136 | 137 | ### OSX ### 138 | *.DS_Store 139 | .AppleDouble 140 | .LSOverride 141 | 142 | # Icon must end with two \r 143 | Icon 144 | # Thumbnails 145 | ._* 146 | # Files that might appear in the root of a volume 147 | .DocumentRevisions-V100 148 | .fseventsd 149 | .Spotlight-V100 150 | .TemporaryItems 151 | .Trashes 152 | .VolumeIcon.icns 153 | .com.apple.timemachine.donotpresent 154 | # Directories potentially created on remote AFP share 155 | .AppleDB 156 | .AppleDesktop 157 | Network Trash Folder 158 | Temporary Items 159 | .apdisk 160 | 161 | # Created by https://www.gitignore.io/api/swiftpackagemanager 162 | # Edit at https://www.gitignore.io/?templates=swiftpackagemanager 163 | 164 | ### SwiftPackageManager ### 165 | Packages 166 | .build/ 167 | xcuserdata 168 | DerivedData/ 169 | *.xcodeproj 170 | 171 | 172 | # End of https://www.gitignore.io/api/swiftpackagemanager 173 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2018 Zhuhao Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CwlCatchException", 6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "7cd2f8cacc4d22f21bc0b2309c3b18acf7957b66", 10 | "version": "1.2.0" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c228db5d2ad1b01ebc84435e823e6cca4e3db98b", 19 | "version": "1.2.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble", 25 | "state": { 26 | "branch": null, 27 | "revision": "b02b00b30b6353632aa4a5fb6124f8147f7140c0", 28 | "version": "8.0.5" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick", 34 | "state": { 35 | "branch": null, 36 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f", 37 | "version": "2.2.0" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Resolver", 8 | products: [ 9 | .library( 10 | name: "Resolver", 11 | targets: ["Resolver"] 12 | ), 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/Quick/Quick", from: "2.2.0"), 16 | .package(url: "https://github.com/Quick/Nimble", from: "8.0.0"), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "Resolver" 21 | ), 22 | .testTarget( 23 | name: "ResolverTests", 24 | dependencies: ["Resolver", "Quick", "Nimble"] 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resolver 2 | 3 | Resolve domain asynchronously 4 | -------------------------------------------------------------------------------- /Sources/Resolver/Resolver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import dnssd 3 | 4 | private let dict = SafeDict() 5 | 6 | public enum ResolveType: DNSServiceProtocol { 7 | case ipv4 = 1, ipv6 = 2, any = 3 8 | } 9 | 10 | public class Resolver { 11 | public static var queue: DispatchQueue { 12 | get { 13 | return _queue 14 | } 15 | set { 16 | _queue.setSpecific(key: queueKey, value: "") 17 | _queue = newValue 18 | _queue.setSpecific(key: queueKey, value: "ResolverQueue") 19 | } 20 | } 21 | 22 | fileprivate static let queueKey = DispatchSpecificKey() 23 | private static var _queue = { 24 | return DispatchQueue(label: "ResolverQueue") 25 | }() 26 | 27 | public static var activeCount: Int { 28 | return dict.count 29 | } 30 | 31 | public let hostname: String 32 | fileprivate let resolveType: ResolveType 33 | fileprivate let firstResult: Bool 34 | public var ipv4Result: [String] = [] 35 | public var ipv6Result: [String] = [] 36 | public var result: [String] { 37 | return ipv4Result + ipv6Result 38 | } 39 | 40 | var cancelled = false 41 | 42 | fileprivate var ref: DNSServiceRef? 43 | fileprivate var id: UnsafeMutablePointer? 44 | fileprivate var completionHandler: ((Resolver?, DNSServiceErrorType?)->())! 45 | fileprivate let timeout: Int 46 | fileprivate let timer = DispatchSource.makeTimerSource(queue: Resolver.queue) 47 | 48 | public static func resolve(hostname: String, qtype: ResolveType = .ipv4, firstResult: Bool = true, timeout: Int = 3, completionHanlder: @escaping (Resolver?, DNSServiceErrorType?)->()) -> Bool { 49 | let resolver = Resolver(hostname: hostname, qtype: qtype, firstResult: firstResult, timeout: timeout) 50 | resolver.completionHandler = completionHanlder 51 | return resolver.resolve() 52 | } 53 | 54 | fileprivate init(hostname: String, qtype: ResolveType, firstResult: Bool, timeout: Int) { 55 | self.hostname = hostname 56 | self.resolveType = qtype 57 | self.firstResult = firstResult 58 | self.timeout = timeout 59 | } 60 | 61 | fileprivate func resolve() -> Bool { 62 | guard ref == nil else { 63 | return false 64 | } 65 | 66 | var result: Bool = false 67 | let action = DispatchWorkItem { 68 | self.id = dict.insert(value: self) 69 | 70 | self.timer.schedule(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(self.timeout)) 71 | self.timer.setEventHandler(handler: self.timeoutHandler) 72 | 73 | result = self.hostname.withCString { (ptr: UnsafePointer) in 74 | guard DNSServiceGetAddrInfo(&self.ref, 0, 0, self.resolveType.rawValue, self.hostname, { (sdRef, flags, interfaceIndex, errorCode, ptr, address, ttl, context) in 75 | // Note this callback block will be called on `Resolver.queue`. 76 | 77 | guard let resolver = dict.get(context!.bindMemory(to: Int.self, capacity: 1)) else { 78 | NSLog("Error: Got some unknown resolver.") 79 | return 80 | } 81 | 82 | guard !resolver.cancelled else { 83 | return 84 | } 85 | 86 | guard errorCode == DNSServiceErrorType(kDNSServiceErr_NoError) else { 87 | resolver.release() 88 | resolver.completionHandler(nil, errorCode) 89 | return 90 | } 91 | 92 | switch (Int32(address!.pointee.sa_family)) { 93 | case AF_INET: 94 | var buffer = [Int8](repeating: 0, count: Int(INET_ADDRSTRLEN)) 95 | _ = buffer.withUnsafeMutableBufferPointer { buf in 96 | address?.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { addr in 97 | var sin_addr = addr.pointee.sin_addr 98 | inet_ntop(AF_INET, &sin_addr, buf.baseAddress, socklen_t(INET_ADDRSTRLEN)) 99 | let addr = String(cString: buf.baseAddress!) 100 | resolver.ipv4Result.append(addr) 101 | } 102 | } 103 | case AF_INET6: 104 | var buffer = [Int8](repeating: 0, count: Int(INET6_ADDRSTRLEN)) 105 | _ = buffer.withUnsafeMutableBufferPointer { buf in 106 | address?.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { addr in 107 | var sin6_addr = addr.pointee.sin6_addr 108 | inet_ntop(AF_INET6, &sin6_addr, buf.baseAddress, socklen_t(INET6_ADDRSTRLEN)) 109 | let addr = String(cString: buf.baseAddress!) 110 | resolver.ipv6Result.append(addr) 111 | } 112 | } 113 | default: 114 | break 115 | } 116 | 117 | if (resolver.firstResult || flags & DNSServiceFlags(kDNSServiceFlagsMoreComing) == 0) { 118 | resolver.release() 119 | return resolver.completionHandler(resolver, nil) 120 | } 121 | }, self.id) == DNSServiceErrorType(kDNSServiceErr_NoError) else { 122 | return false 123 | } 124 | 125 | DNSServiceSetDispatchQueue(self.ref, Resolver.queue) 126 | self.timer.resume() 127 | return true 128 | } 129 | } 130 | 131 | if DispatchQueue.getSpecific(key: Resolver.queueKey) == "ResolverQueue" { 132 | action.perform() 133 | } else { 134 | Resolver.queue.sync(execute: action) 135 | } 136 | 137 | return result 138 | } 139 | 140 | func timeoutHandler() { 141 | if !cancelled { 142 | release() 143 | completionHandler(nil, DNSServiceErrorType(kDNSServiceErr_Timeout)) 144 | } 145 | } 146 | 147 | func release() { 148 | cancelled = true 149 | 150 | timer.cancel() 151 | 152 | if ref != nil { 153 | DNSServiceRefDeallocate(ref) 154 | ref = nil 155 | } 156 | if id != nil { 157 | _ = dict.remove(id!) 158 | id = nil 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Sources/Resolver/SafeDict.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | 4 | /// This class is not thread-safe. 5 | class SafeDict { 6 | private var dict: [Int:T] = [:] 7 | private var curr = 0 8 | 9 | var count: Int { 10 | return dict.count 11 | } 12 | 13 | func insert(value: T) -> UnsafeMutablePointer { 14 | let ptr = UnsafeMutablePointer.allocate(capacity: 1) 15 | ptr.pointee = curr 16 | dict[curr] = value 17 | curr += 1 18 | return ptr 19 | } 20 | 21 | func get(_ id: Int) -> T? { 22 | return dict[id] 23 | } 24 | 25 | func get(_ id: UnsafePointer) -> T? { 26 | return get(id.pointee) 27 | } 28 | 29 | func remove(_ id: Int) -> T? { 30 | return dict.removeValue(forKey: id) 31 | } 32 | 33 | func remove(_ id: UnsafeMutablePointer) -> T? { 34 | defer { 35 | id.deallocate() 36 | } 37 | return remove(id.pointee) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Tests/ResolverTests/ResolverTests.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | @testable import Resolver 4 | import dnssd 5 | 6 | class ResolverSpec: QuickSpec { 7 | override func spec() { 8 | describe("The resolve method") { 9 | context("If it is asked to resolve IPv4 address") { 10 | it("gives one IP") { 11 | var result: Resolver? 12 | var errorCode: DNSServiceErrorType? 13 | waitUntil(timeout: 6) { done in 14 | expect(Resolver.resolve(hostname: "cloudflare.com") { res, error in 15 | result = res 16 | errorCode = error 17 | done() 18 | }) == true 19 | } 20 | expect(result?.result.count) == 1 21 | expect(result?.ipv4Result.count) == 1 22 | expect(errorCode).to(beNil()) 23 | expect(Resolver.activeCount) == 0 24 | } 25 | 26 | it("gives all IPs") { 27 | var result: Resolver? 28 | var errorCode: DNSServiceErrorType? 29 | waitUntil(timeout: 6) { done in 30 | expect(Resolver.resolve(hostname: "cloudflare.com", firstResult: false) { res, error in 31 | result = res 32 | errorCode = error 33 | done() 34 | }) == true 35 | } 36 | expect(result?.result.count) > 1 37 | expect(result?.ipv4Result.count) > 1 38 | expect(errorCode).to(beNil()) 39 | expect(Resolver.activeCount) == 0 40 | } 41 | } 42 | 43 | context("If it is asked to resolve IPv6 address") { 44 | it("gives one IP") { 45 | var result: Resolver? 46 | var errorCode: DNSServiceErrorType? 47 | waitUntil(timeout: 6) { done in 48 | expect(Resolver.resolve(hostname: "cloudflare.com", qtype: .ipv6) { res, error in 49 | result = res 50 | errorCode = error 51 | done() 52 | }) == true 53 | } 54 | expect(result?.result.count) == 1 55 | expect(result?.ipv6Result.count) == 1 56 | expect(errorCode).to(beNil()) 57 | expect(Resolver.activeCount) == 0 58 | } 59 | 60 | it("gives all IPs") { 61 | var result: Resolver? 62 | var errorCode: DNSServiceErrorType? 63 | waitUntil(timeout: 6) { done in 64 | expect(Resolver.resolve(hostname: "cloudflare.com", qtype: .ipv6, firstResult: false) { res, error in 65 | result = res 66 | errorCode = error 67 | done() 68 | }) == true 69 | } 70 | expect(result?.result.count) > 1 71 | expect(result?.ipv6Result.count) > 1 72 | expect(errorCode).to(beNil()) 73 | expect(Resolver.activeCount) == 0 74 | } 75 | } 76 | 77 | context("When it is given an invalid domain") { 78 | it("gives error") { 79 | var result: Resolver? 80 | var errorCode: DNSServiceErrorType? 81 | waitUntil(timeout: 6) { done in 82 | expect(Resolver.resolve(hostname: "0.cloudflare.com") { res, error in 83 | result = res 84 | errorCode = error 85 | done() 86 | }) == true 87 | } 88 | expect(result).to(beNil()) 89 | expect(errorCode).toNot(beNil()) 90 | expect(Resolver.activeCount) == 0 91 | } 92 | } 93 | 94 | it("resolves queries asynchronously") { 95 | var count = 100 96 | waitUntil(timeout: 8) { done in 97 | let semaphore = DispatchSemaphore(value: 1) 98 | for i in 0..