CFHostSampleSwift/HostNameQuery.swift
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Resolves an IP address to a list of DNS names. |
*/ |
import Foundation |
/// This class uses CFHost to query an IP address for its DNS names. To do this: |
/// |
/// 1. Create the `HostNameQuery` object with the IP address in question. |
/// |
/// 2. Set a delegate. |
/// |
/// 3. Call `start()`. |
/// |
/// 4. Wait for `didComplete(names:hostNameQuery:)` or `didComplete(error:hostNameQuery:)` |
/// to be called. |
/// |
/// CFHost, and hence this class, is run loop based. The class remembers the run loop on which you |
/// call `start()` and delivers the delegate callbacks on that run loop. |
/// |
/// - Important: Reverse DNS queries are notoriously unreliable. Specifically, you must not |
/// assume that: |
/// |
/// - Every IP address has a valid reverse DNS name |
/// - The reverse DNS name is unique |
/// - There's any correlation between the forward and reverse DNS mappings |
/// |
/// Unless you have domain specific knowledge (for example, you're working in an enterprise |
/// environment where you know how the DNS is set up), reverse DNS queries are generally not |
/// useful for anything other than logging. |
final class HostNameQuery { |
/// Creates an instance to query the specified IP address for its DNS name. |
/// |
/// - Parameter address: The IP address to query, as a `Data` value containing some flavour of `sockaddr`. |
init(address: Data) { |
self.address = address |
self.host = CFHostCreateWithAddress(nil, address as NSData).takeRetainedValue() |
} |
/// The IP address to query, as a `Data` value containing some flavour of `sockaddr`. |
let address: Data |
/// You must set this to learn about the results of your query. |
weak var delegate: HostNameQueryDelegate? = nil |
/// Starts the query process. |
/// |
/// The query remembers the thread that called this method and calls any delegate |
/// callbacks on that thread. |
/// |
/// - Important: For the query to make progress, this thread must run its run loop in |
/// the default run loop mode. |
/// |
/// - Warning: It is an error to start a query that's running. |
func start() { |
precondition(self.targetRunLoop == nil) |
self.targetRunLoop = RunLoop.current |
var context = CFHostClientContext() |
context.info = Unmanaged.passRetained(self).toOpaque() |
var success = CFHostSetClient(self.host, { (_ host: CFHost, _: CFHostInfoType, _ streamErrorPtr: UnsafePointer<CFStreamError>?, _ info: UnsafeMutableRawPointer?) in |
let obj = Unmanaged<HostNameQuery>.fromOpaque(info!).takeUnretainedValue() |
if let streamError = streamErrorPtr?.pointee, (streamError.domain != 0 || streamError.error != 0) { |
obj.stop(streamError: streamError, notify: true) |
} else { |
obj.stop(streamError: nil, notify: true) |
} |
}, &context) |
assert(success) |
CFHostScheduleWithRunLoop(self.host, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) |
var streamError = CFStreamError() |
success = CFHostStartInfoResolution(self.host, .names, &streamError) |
if !success { |
self.stop(streamError: streamError, notify: true) |
} |
} |
/// Stops the query with the supplied error, notifying the delegate if `notify` is true. |
private func stop(streamError: CFStreamError?, notify: Bool) { |
let error: Error? |
if let streamError = streamError { |
// Convert a CFStreamError to a NSError. This is less than ideal because I only handle |
// a limited number of error domains. Wouldn't it be nice if there was a public API to |
// do this mapping <rdar://problem/5845848> or a CFHost API that used CFError |
// <rdar://problem/6016542>. |
switch streamError.domain { |
case CFStreamErrorDomain.POSIX.rawValue: |
error = NSError(domain: NSPOSIXErrorDomain, code: Int(streamError.error)) |
case CFStreamErrorDomain.macOSStatus.rawValue: |
error = NSError(domain: NSOSStatusErrorDomain, code: Int(streamError.error)) |
case Int(kCFStreamErrorDomainNetServices): |
error = NSError(domain: kCFErrorDomainCFNetwork as String, code: Int(streamError.error)) |
case Int(kCFStreamErrorDomainNetDB): |
error = NSError(domain: kCFErrorDomainCFNetwork as String, code: Int(CFNetworkErrors.cfHostErrorUnknown.rawValue), userInfo: [ |
kCFGetAddrInfoFailureKey as String: streamError.error as NSNumber |
]) |
default: |
// If it's something we don't understand, we just assume it comes from |
// CFNetwork. |
error = NSError(domain: kCFErrorDomainCFNetwork as String, code: Int(streamError.error)) |
} |
} else { |
error = nil |
} |
self.stop(error: error, notify: notify) |
} |
/// Stops the query with the supplied error, notifying the delegate if `notify` is true. |
private func stop(error: Error?, notify: Bool) { |
precondition(RunLoop.current == self.targetRunLoop) |
self.targetRunLoop = nil |
CFHostSetClient(self.host, nil, nil) |
CFHostUnscheduleFromRunLoop(self.host, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue) |
CFHostCancelInfoResolution(self.host, .names) |
Unmanaged.passUnretained(self).release() |
if notify { |
if let error = error { |
self.delegate?.didComplete(error: error, hostNameQuery: self) |
} else { |
let names = CFHostGetNames(self.host, nil)!.takeUnretainedValue() as NSArray as! [String] |
self.delegate?.didComplete(names: names, hostNameQuery: self) |
} |
} |
} |
/// Cancels a running query. |
/// |
/// If you successfully cancel a query, no delegate callback for that query will be |
/// called. |
/// |
/// If the query is running, you must call this from the thread that called `start()`. |
/// |
/// - Note: It is acceptable to call this on a query that's not running; it does nothing |
// in that case. |
func cancel() { |
if self.targetRunLoop != nil { |
self.stop(error: NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil), notify: false) |
} |
} |
/// The underlying CFHost object that does the resolution. |
private var host: CFHost |
/// The run loop on which the CFHost object is scheduled; this is set in `start()` |
/// and cleared when the query stops (either via `cancel()` or by completing). |
private var targetRunLoop: RunLoop? = nil |
} |
/// The delegate protocol for the HostNameQuery class. |
protocol HostNameQueryDelegate: class { |
/// Called when the query completes successfully. |
/// |
/// This is called on the same thread that called `start()`. |
/// |
/// - Parameters: |
/// - names: The DNS names for the IP address. |
/// - query: The query that completed. |
func didComplete(names: [String], hostNameQuery query: HostNameQuery) |
/// Called when the query completes with an error. |
/// |
/// This is called on the same thread that called `start()`. |
/// |
/// - Parameters: |
/// - error: An error describing the failure. |
/// - query: The query that completed. |
/// |
/// - Important: In most cases the error will be in domain `kCFErrorDomainCFNetwork` |
/// with a code of `kCFHostErrorUnknown` (aka `CFNetworkErrors.cfHostErrorUnknown`), |
/// and the user info dictionary will contain an element with the `kCFGetAddrInfoFailureKey` |
/// key whose value is an NSNumber containing an `EAI_XXX` value (from `<netdb.h>`). |
func didComplete(error: Error, hostNameQuery query: HostNameQuery) |
} |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-03-14