DNS NULL record support in MYBonjourRegistration. Minor fix to IPAddress init. Force 4-char indent in source files.
2 // MYBonjourRegistration.m
5 // Created by Jens Alfke on 4/27/09.
6 // Copyright 2009 Jens Alfke. All rights reserved.
9 #import "MYBonjourRegistration.h"
10 #import "MYBonjourService.h"
11 #import "ExceptionUtils.h"
17 #define kTXTTTL 60 // TTL in seconds for TXT records I register
20 @interface MYBonjourRegistration ()
21 @property BOOL registered;
22 - (void) _updateNullRecord;
26 @implementation MYBonjourRegistration
29 static NSMutableDictionary *sAllRegistrations;
32 + (void) priv_addRegistration: (MYBonjourRegistration*)reg {
33 if (!sAllRegistrations)
34 sAllRegistrations = [[NSMutableDictionary alloc] init];
35 [sAllRegistrations setObject: reg forKey: reg.fullName];
38 + (void) priv_removeRegistration: (MYBonjourRegistration*)reg {
39 [sAllRegistrations removeObjectForKey: reg.fullName];
42 + (MYBonjourRegistration*) registrationWithFullName: (NSString*)fullName {
43 return [sAllRegistrations objectForKey: fullName];
47 - (id) initWithServiceType: (NSString*)serviceType port: (UInt16)port
51 self.continuous = YES;
52 self.usePrivateConnection = YES; // DNSServiceUpdateRecord doesn't work with shared conn :(
53 _type = [serviceType copy];
69 @synthesize name=_name, type=_type, domain=_domain, port=_port, autoRename=_autoRename;
70 @synthesize registered=_registered;
73 - (NSString*) fullName {
74 return [[self class] fullNameOfService: _name ofType: _type inDomain: _domain];
78 - (BOOL) isSameAsService: (MYBonjourService*)service {
79 return _name && _domain && [self.fullName isEqualToString: service.fullName];
83 - (NSString*) description
85 return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
89 - (void) priv_registeredAsName: (NSString*)name
90 type: (NSString*)regtype
91 domain: (NSString*)domain
93 if (!$equal(name,_name))
95 if (!$equal(domain,_domain))
97 LogTo(Bonjour,@"Registered %@", self);
98 self.registered = YES;
99 [[self class] priv_addRegistration: self];
103 static void regCallback(DNSServiceRef sdRef,
104 DNSServiceFlags flags,
105 DNSServiceErrorType errorCode,
111 MYBonjourRegistration *reg = context;
114 [reg priv_registeredAsName: [NSString stringWithUTF8String: name]
115 type: [NSString stringWithUTF8String: regtype]
116 domain: [NSString stringWithUTF8String: domain]];
117 }catchAndReport(@"MYBonjourRegistration callback");
118 [reg gotResponse: errorCode];
122 - (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
123 DNSServiceFlags flags = 0;
125 flags |= kDNSServiceFlagsNoAutoRename;
126 NSData *txtData = nil;
128 txtData = [NSNetService dataFromTXTRecordDictionary: _txtRecord];
129 DNSServiceErrorType err;
130 err = DNSServiceRegister(sdRefPtr,
133 _name.UTF8String, // _name is likely to be nil
135 _domain.UTF8String, // _domain is most likely nil
142 if (!err && _nullRecord)
143 [self _updateNullRecord];
149 if (_nullRecordReg && self.serviceRef) {
150 DNSServiceRemoveRecord(self.serviceRef, _nullRecordReg, 0);
151 _nullRecordReg = NULL;
157 [[self class] priv_removeRegistration: self];
158 self.registered = NO;
163 + (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
166 // First translate any non-NSData values into UTF-8 formatted description data:
167 NSMutableDictionary *encodedDict = $mdict();
168 for (NSString *key in txtDict) {
169 id value = [txtDict objectForKey: key];
170 if (![value isKindOfClass: [NSData class]]) {
171 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
173 [encodedDict setObject: value forKey: key];
175 return [NSNetService dataFromTXTRecordDictionary: encodedDict];
179 static int compareData (id data1, id data2, void *context) {
180 size_t length1 = [data1 length], length2 = [data2 length];
181 int result = memcmp([data1 bytes], [data2 bytes], MIN(length1,length2));
185 else if (length1<length2)
191 + (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
196 // First convert keys and values to NSData:
197 NSMutableDictionary *dataDict = $mdict();
198 for (NSString *key in txtDict) {
199 if (![key hasPrefix: @"("]) { // ignore parenthesized keys
200 if (![key isKindOfClass: [NSString class]]) {
201 Warn(@"TXT dictionary cannot have %@ as key", [key class]);
204 NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
205 if (keyData.length > 255) {
206 Warn(@"TXT dictionary key too long: %@", key);
209 id value = [txtDict objectForKey: key];
210 if (![value isKindOfClass: [NSData class]]) {
211 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
213 if ([value length] > 255) {
214 Warn(@"TXT dictionary value too long: %@", value);
217 [dataDict setObject: value forKey: keyData];
221 // Add key/value pairs, sorted by increasing key:
222 NSMutableData *canonical = [NSMutableData dataWithCapacity: 1000];
223 for (NSData *key in [[dataDict allKeys] sortedArrayUsingFunction: compareData context: NULL]) {
224 // Append key prefixed with length:
225 UInt8 length = [key length];
226 [canonical appendBytes: &length length: sizeof(length)];
227 [canonical appendData: key];
228 // Append value prefixed with length:
229 NSData *value = [dataDict objectForKey: key];
230 length = [value length];
231 [canonical appendBytes: &length length: sizeof(length)];
232 [canonical appendData: value];
238 - (void) updateTxtRecord {
239 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
240 if (self.serviceRef) {
241 NSData *data = [[self class] dataFromTXTRecordDictionary: _txtRecord];
242 Assert(data!=nil || _txtRecord==nil, @"Can't convert dictionary to TXT record: %@", _txtRecord);
243 DNSServiceErrorType err = DNSServiceUpdateRecord(self.serviceRef,
250 Warn(@"%@ failed to update TXT (err=%i)", self,err);
252 LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
257 - (NSDictionary*) txtRecord {
261 - (void) setTxtRecord: (NSDictionary*)txtDict {
262 if (!$equal(_txtRecord,txtDict)) {
263 setObjCopy(&_txtRecord, txtDict);
264 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
265 [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
269 - (void) setString: (NSString*)value forTxtKey: (NSString*)key
271 NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
272 if (!$equal(data, [_txtRecord objectForKey: key])) {
274 if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
275 [_txtRecord setObject: data forKey: key];
277 [_txtRecord removeObjectForKey: key];
278 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
279 [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
284 - (NSData*) nullRecord {
288 - (void) setNullRecord: (NSData*)nullRecord {
289 if (ifSetObj(&_nullRecord, nullRecord))
291 [self _updateNullRecord];
295 - (void) _updateNullRecord {
296 DNSServiceRef serviceRef = self.serviceRef;
298 DNSServiceErrorType err = 0;
300 if (_nullRecordReg) {
301 err = DNSServiceRemoveRecord(serviceRef, _nullRecordReg, 0);
302 _nullRecordReg = NULL;
304 } else if (!_nullRecordReg) {
305 err = DNSServiceAddRecord(serviceRef, &_nullRecordReg, 0,
306 kDNSServiceType_NULL,
307 _nullRecord.length, _nullRecord.bytes,
310 err = DNSServiceUpdateRecord(serviceRef, _nullRecordReg, 0,
311 _nullRecord.length, _nullRecord.bytes,
315 Warn(@"MYBonjourRegistration: Couldn't update NULL record, err=%i",err);
317 LogTo(DNS, @"MYBonjourRegistration: Set NULL record (%u bytes) %@",
318 _nullRecord.length, _nullRecord);
327 #pragma mark TESTING:
331 #import "MYBonjourQuery.h"
332 #import "MYAddressLookup.h"
334 @interface BonjourRegTester : NSObject
336 MYBonjourRegistration *_reg;
341 @implementation BonjourRegTester
344 NSDictionary *txt = $dict({@"time", $sprintf(@"%.3lf", CFAbsoluteTimeGetCurrent())});
345 _reg.txtRecord = txt;
346 CAssertEqual(_reg.txtRecord, txt);
347 [self performSelector: @selector(updateTXT) withObject: nil afterDelay: 3.0];
354 _reg = [[MYBonjourRegistration alloc] initWithServiceType: @"_foo._tcp" port: 12345];
355 [_reg addObserver: self forKeyPath: @"registered" options: NSKeyValueObservingOptionNew context: NULL];
356 [_reg addObserver: self forKeyPath: @"name" options: NSKeyValueObservingOptionNew context: NULL];
367 [_reg removeObserver: self forKeyPath: @"registered"];
368 [_reg removeObserver: self forKeyPath: @"name"];
373 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
375 LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
380 TestCase(BonjourReg) {
381 EnableLogTo(Bonjour,YES);
382 EnableLogTo(DNS,YES);
383 [NSRunLoop currentRunLoop]; // create runloop
384 BonjourRegTester *tester = [[BonjourRegTester alloc] init];
385 [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 15]];
393 Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
395 Redistribution and use in source and binary forms, with or without modification, are permitted
396 provided that the following conditions are met:
398 * Redistributions of source code must retain the above copyright notice, this list of conditions
399 and the following disclaimer.
400 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
401 and the following disclaimer in the documentation and/or other materials provided with the
404 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
405 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
406 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
407 BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
408 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
409 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
410 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
411 THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.