* MYBonjourBrowser: Added delegate (no methods for it yet, just for client use.)
* MYBonjourRegistration: Added +canonicalFormOfTXTRecordDictionary:.
* MYBonjourService: Added back-reference to browser.
* IPAddress: Added conversions to/from struct sockaddr.
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;
25 @implementation MYBonjourRegistration
28 static NSMutableDictionary *sAllRegistrations;
31 + (void) priv_addRegistration: (MYBonjourRegistration*)reg {
32 if (!sAllRegistrations)
33 sAllRegistrations = [[NSMutableDictionary alloc] init];
34 [sAllRegistrations setObject: reg forKey: reg.fullName];
37 + (void) priv_removeRegistration: (MYBonjourRegistration*)reg {
38 [sAllRegistrations removeObjectForKey: reg.fullName];
41 + (MYBonjourRegistration*) registrationWithFullName: (NSString*)fullName {
42 return [sAllRegistrations objectForKey: fullName];
46 - (id) initWithServiceType: (NSString*)serviceType port: (UInt16)port
50 self.continuous = YES;
51 self.usePrivateConnection = YES; // DNSServiceUpdateRecord doesn't work with shared conn :(
52 _type = [serviceType copy];
68 @synthesize name=_name, type=_type, domain=_domain, port=_port, autoRename=_autoRename;
69 @synthesize registered=_registered;
72 - (NSString*) fullName {
73 return [[self class] fullNameOfService: _name ofType: _type inDomain: _domain];
77 - (BOOL) isSameAsService: (MYBonjourService*)service {
78 return _name && _domain && [self.fullName isEqualToString: service.fullName];
82 - (NSString*) description
84 return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
88 - (void) priv_registeredAsName: (NSString*)name
89 type: (NSString*)regtype
90 domain: (NSString*)domain
92 if (!$equal(name,_name))
94 if (!$equal(domain,_domain))
96 LogTo(Bonjour,@"Registered %@", self);
97 self.registered = YES;
98 [[self class] priv_addRegistration: self];
102 static void regCallback(DNSServiceRef sdRef,
103 DNSServiceFlags flags,
104 DNSServiceErrorType errorCode,
110 MYBonjourRegistration *reg = context;
113 [reg priv_registeredAsName: [NSString stringWithUTF8String: name]
114 type: [NSString stringWithUTF8String: regtype]
115 domain: [NSString stringWithUTF8String: domain]];
116 }catchAndReport(@"MYBonjourRegistration callback");
117 [reg gotResponse: errorCode];
121 - (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
122 DNSServiceFlags flags = 0;
124 flags |= kDNSServiceFlagsNoAutoRename;
125 NSData *txtData = nil;
127 txtData = [NSNetService dataFromTXTRecordDictionary: _txtRecord];
128 return DNSServiceRegister(sdRefPtr,
131 _name.UTF8String, // _name is likely to be nil
133 _domain.UTF8String, // _domain is most likely nil
146 [[self class] priv_removeRegistration: self];
147 self.registered = NO;
152 + (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
155 // First translate any non-NSData values into UTF-8 formatted description data:
156 NSMutableDictionary *encodedDict = $mdict();
157 for (NSString *key in txtDict) {
158 id value = [txtDict objectForKey: key];
159 if (![value isKindOfClass: [NSData class]]) {
160 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
162 [encodedDict setObject: value forKey: key];
164 return [NSNetService dataFromTXTRecordDictionary: encodedDict];
168 static int compareData (id data1, id data2, void *context) {
169 size_t length1 = [data1 length], length2 = [data2 length];
170 int result = memcmp([data1 bytes], [data2 bytes], MIN(length1,length2));
174 else if (length1<length2)
180 + (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
185 // First convert keys and values to NSData:
186 NSMutableDictionary *dataDict = $mdict();
187 for (NSString *key in txtDict) {
188 if (![key hasPrefix: @"("]) { // ignore parenthesized keys
189 if (![key isKindOfClass: [NSString class]]) {
190 Warn(@"TXT dictionary cannot have %@ as key", [key class]);
193 NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
194 if (keyData.length > 255) {
195 Warn(@"TXT dictionary key too long: %@", key);
198 id value = [txtDict objectForKey: key];
199 if (![value isKindOfClass: [NSData class]]) {
200 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
202 if ([value length] > 255) {
203 Warn(@"TXT dictionary value too long: %@", value);
206 [dataDict setObject: value forKey: keyData];
210 // Add key/value pairs, sorted by increasing key:
211 NSMutableData *canonical = [NSMutableData dataWithCapacity: 1000];
212 for (NSData *key in [[dataDict allKeys] sortedArrayUsingFunction: compareData context: NULL]) {
213 // Append key prefixed with length:
214 UInt8 length = [key length];
215 [canonical appendBytes: &length length: sizeof(length)];
216 [canonical appendData: key];
217 // Append value prefixed with length:
218 NSData *value = [dataDict objectForKey: key];
219 length = [value length];
220 [canonical appendBytes: &length length: sizeof(length)];
221 [canonical appendData: value];
227 - (void) updateTxtRecord {
228 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
229 if (self.serviceRef) {
230 NSData *data = [[self class] dataFromTXTRecordDictionary: _txtRecord];
231 Assert(data!=nil || _txtRecord==nil, @"Can't convert dictionary to TXT record: %@", _txtRecord);
232 DNSServiceErrorType err = DNSServiceUpdateRecord(self.serviceRef,
239 Warn(@"%@ failed to update TXT (err=%i)", self,err);
241 LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
246 - (NSDictionary*) txtRecord {
250 - (void) setTxtRecord: (NSDictionary*)txtDict {
251 if (!$equal(_txtRecord,txtDict)) {
252 setObjCopy(&_txtRecord, txtDict);
253 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
254 [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
258 - (void) setString: (NSString*)value forTxtKey: (NSString*)key
260 NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
261 if (!$equal(data, [_txtRecord objectForKey: key])) {
263 if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
264 [_txtRecord setObject: data forKey: key];
266 [_txtRecord removeObjectForKey: key];
267 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
268 [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
278 #pragma mark TESTING:
282 #import "MYBonjourQuery.h"
283 #import "MYAddressLookup.h"
285 @interface BonjourRegTester : NSObject
287 MYBonjourRegistration *_reg;
292 @implementation BonjourRegTester
295 NSDictionary *txt = $dict({@"time", $sprintf(@"%.3lf", CFAbsoluteTimeGetCurrent())});
296 _reg.txtRecord = txt;
297 CAssertEqual(_reg.txtRecord, txt);
298 [self performSelector: @selector(updateTXT) withObject: nil afterDelay: 3.0];
305 _reg = [[MYBonjourRegistration alloc] initWithServiceType: @"_foo._tcp" port: 12345];
306 [_reg addObserver: self forKeyPath: @"registered" options: NSKeyValueObservingOptionNew context: NULL];
307 [_reg addObserver: self forKeyPath: @"name" options: NSKeyValueObservingOptionNew context: NULL];
318 [_reg removeObserver: self forKeyPath: @"registered"];
319 [_reg removeObserver: self forKeyPath: @"name"];
324 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
326 LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
331 TestCase(BonjourReg) {
332 EnableLogTo(Bonjour,YES);
333 EnableLogTo(DNS,YES);
334 [NSRunLoop currentRunLoop]; // create runloop
335 BonjourRegTester *tester = [[BonjourRegTester alloc] init];
336 [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 15]];
344 Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
346 Redistribution and use in source and binary forms, with or without modification, are permitted
347 provided that the following conditions are met:
349 * Redistributions of source code must retain the above copyright notice, this list of conditions
350 and the following disclaimer.
351 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
352 and the following disclaimer in the documentation and/or other materials provided with the
355 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
356 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
357 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
358 BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
359 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
360 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
361 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
362 THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.