-
-
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) | |
| } | |
| } |
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
Thanks for this, but there's an issue some people may have. This code:
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
txtPtrstarts with a null. Other record types may need other changes. See RFC 1035 for details.