-
-
Save fikeminkel/a9c4bc4d0348527e8df3690e242038d3 to your computer and use it in GitHub Desktop.
import dnssd | |
struct DNSTxtRecord { | |
typealias DNSLookupHandler = ([String: String]?) -> Void | |
static func lookup(_ domainName: String, completionHandler: @escaping DNSLookupHandler) { | |
var mutableCompletionHandler = completionHandler // completionHandler needs to be mutable to be used as inout param | |
let callback: DNSServiceQueryRecordReply = { | |
(sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context) -> Void in | |
// dereference completionHandler from pointer since we can't directly capture it in a C callback | |
guard let completionHandlerPtr = context?.assumingMemoryBound(to: DNSLookupHandler.self) else { return } | |
let completionHandler = completionHandlerPtr.pointee | |
// map memory at rdata to a UInt8 pointer | |
guard let txtPtr = rdata?.assumingMemoryBound(to: UInt8.self) else { | |
completionHandler(nil) | |
return | |
} | |
// advancing pointer by 1 to skip bad character at beginning of record | |
let txt = String(cString: txtPtr.advanced(by: 1)) | |
// parse name=value txt record into dictionary | |
var record: [String: String] = [:] | |
let recordParts = txt.components(separatedBy: "=") | |
record[recordParts[0]] = recordParts[1] | |
completionHandler(record) | |
} | |
// MemoryLayout<T>.size can give us the necessary size of the struct to allocate | |
let serviceRef: UnsafeMutablePointer<DNSServiceRef?> = UnsafeMutablePointer.allocate(capacity: MemoryLayout<DNSServiceRef>.size) | |
// pass completionHandler as context object to callback so that we have a way to pass the record result back to the caller | |
DNSServiceQueryRecord(serviceRef, 0, 0, domainName, UInt16(kDNSServiceType_TXT), UInt16(kDNSServiceClass_IN), callback, &mutableCompletionHandler); | |
DNSServiceProcessResult(serviceRef.pointee) | |
DNSServiceRefDeallocate(serviceRef.pointee) | |
} | |
} |
Thanks for this, but there's an issue some people may have. This code:
// advancing pointer by 1 to skip bad character at beginning of record
let txt = String(cString: txtPtr.advanced(by: 1))
The pointer needs to be advanced, but the number to skip depends on the type of record. For example when looking up MX records, the first two bytes are a 16-bit integer indicating record preference. So for MX the pointer needs to advance by 2. Without that, you can end up getting empty strings, since advancing by 1 can still mean that txtPtr
starts with a null. Other record types may need other changes. See RFC 1035 for details.
Adapted the above version to the following which works for me. Had to add some error handling and a timeout, see below: kDNSServiceErr_NoError
, kDNSServiceFlagsTimeout
import Foundation
import dnssd
// ...
typealias DNSLookupHandler = ([String: String]?) -> Void
func query(domainName: String) -> [String: String]? {
var result: [String: String] = [:]
var recordHandler: DNSRecordHandler = {
(record) -> Void in
if (record != nil) {
for (k, v) in record! {
result.updateValue(v, forKey: k)
}
}
}
let callback: DNSServiceQueryRecordReply = {
(sdRef, flags, interfaceIndex, errorCode, fullname, rrtype, rrclass, rdlen, rdata, ttl, context) -> Void in
guard let handlerPtr = context?.assumingMemoryBound(to: DNSLookupHandler.self) else {
return
}
let handler = handlerPtr.pointee
if (errorCode != kDNSServiceErr_NoError) {
return
}
guard let txtPtr = rdata?.assumingMemoryBound(to: UInt8.self) else {
return
}
let txt = String(cString: txtPtr.advanced(by: 1))
var record: [String: String] = [:]
let parts = txt.components(separatedBy: "=")
record[parts[0]] = parts[1]
handler(record)
}
let serviceRef: UnsafeMutablePointer<DNSServiceRef?> = UnsafeMutablePointer.allocate(capacity: MemoryLayout<DNSServiceRef>.size)
let code = DNSServiceQueryRecord(serviceRef, kDNSServiceFlagsTimeout, 0, domainName, UInt16(kDNSServiceType_TXT), UInt16(kDNSServiceClass_IN), callback, &recordHandler)
if (code != kDNSServiceErr_NoError) {
return nil
}
DNSServiceProcessResult(serviceRef.pointee)
DNSServiceRefDeallocate(serviceRef.pointee)
return result
}
@mosen or anyone looking for an example parsing SRV records, heres an example I found that worked very well for me: https://github.com/jamf/NoMAD-2/blob/main/NoMAD/SRVLookups/SRVResolver.swift
Thank you very much! @fikeminkel and @juanheyns both snipped worked for me.
If someone is having an issue like @ethan-gerardot where it gets stuck at DNSServiceProcessResult
, is most likely because the domainName
is invalid, adding a time out like @juanheyns might help.
https://gist.github.com/fikeminkel/a9c4bc4d0348527e8df3690e242038d3#file-dnstxtrecord-swift-L20
Unfortunately this code produces incorrect behaviour on concatenated TXT entries, leaving a garbage byte in place of the concatenation:
// advancing pointer by 1 to skip bad character at beginning of record
let txt = String(cString: txtPtr.advanced(by: 1))
Quoting https://kb.isc.org/docs/aa-00356: per RFC 4408 a TXT or SPF record is allowed to contain multiple strings, which should be concatenated together by the reading application
See apple/swift-async-dns-resolver#43 and apple/swift-async-dns-resolver#44
@ethan-gerardot I'm looking at this, trying to use it to resolve SRV records. I don't really have any knowledge of socket programming but i did see a mention about using select() in the obj-c implementation. Is there any chance you have example code with the finished implementation?