Bonjour/MYBonjourRegistration.m
author Jens Alfke <jens@mooseyard.com>
Mon Jul 20 14:50:49 2009 -0700 (2009-07-20)
changeset 60 dd637bdd214e
parent 59 46c7844cb592
child 62 8713f2d6a4c5
permissions -rw-r--r--
DNS NULL record support in MYBonjourRegistration. Minor fix to IPAddress init. Force 4-char indent in source files.
     1 //
     2 //  MYBonjourRegistration.m
     3 //  MYNetwork
     4 //
     5 //  Created by Jens Alfke on 4/27/09.
     6 //  Copyright 2009 Jens Alfke. All rights reserved.
     7 //
     8 
     9 #import "MYBonjourRegistration.h"
    10 #import "MYBonjourService.h"
    11 #import "ExceptionUtils.h"
    12 #import "Test.h"
    13 #import "Logging.h"
    14 #import <dns_sd.h>
    15 
    16 
    17 #define kTXTTTL 60          // TTL in seconds for TXT records I register
    18 
    19 
    20 @interface MYBonjourRegistration ()
    21 @property BOOL registered;
    22 - (void) _updateNullRecord;
    23 @end
    24 
    25 
    26 @implementation MYBonjourRegistration
    27 
    28 
    29 static NSMutableDictionary *sAllRegistrations;
    30 
    31 
    32 + (void) priv_addRegistration: (MYBonjourRegistration*)reg {
    33     if (!sAllRegistrations)
    34         sAllRegistrations = [[NSMutableDictionary alloc] init];
    35     [sAllRegistrations setObject: reg forKey: reg.fullName];
    36 }
    37 
    38 + (void) priv_removeRegistration: (MYBonjourRegistration*)reg {
    39     [sAllRegistrations removeObjectForKey: reg.fullName];
    40 }
    41 
    42 + (MYBonjourRegistration*) registrationWithFullName: (NSString*)fullName {
    43     return [sAllRegistrations objectForKey: fullName];
    44 }
    45 
    46 
    47 - (id) initWithServiceType: (NSString*)serviceType port: (UInt16)port
    48 {
    49     self = [super init];
    50     if (self != nil) {
    51         self.continuous = YES;
    52         self.usePrivateConnection = YES;    // DNSServiceUpdateRecord doesn't work with shared conn :(
    53         _type = [serviceType copy];
    54         _port = port;
    55         _autoRename = YES;
    56     }
    57     return self;
    58 }
    59 
    60 - (void) dealloc {
    61     [_name release];
    62     [_type release];
    63     [_domain release];
    64     [_txtRecord release];
    65     [super dealloc];
    66 }
    67 
    68 
    69 @synthesize name=_name, type=_type, domain=_domain, port=_port, autoRename=_autoRename;
    70 @synthesize registered=_registered;
    71 
    72 
    73 - (NSString*) fullName {
    74     return [[self class] fullNameOfService: _name ofType: _type inDomain: _domain];
    75 }
    76 
    77 
    78 - (BOOL) isSameAsService: (MYBonjourService*)service {
    79     return _name && _domain && [self.fullName isEqualToString: service.fullName];
    80 }
    81 
    82 
    83 - (NSString*) description
    84 {
    85     return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
    86 }
    87 
    88 
    89 - (void) priv_registeredAsName: (NSString*)name 
    90                           type: (NSString*)regtype
    91                         domain: (NSString*)domain
    92 {
    93     if (!$equal(name,_name))
    94         self.name = name;
    95     if (!$equal(domain,_domain))
    96         self.domain = domain;
    97     LogTo(Bonjour,@"Registered %@", self);
    98     self.registered = YES;
    99     [[self class] priv_addRegistration: self];
   100 }
   101 
   102 
   103 static void regCallback(DNSServiceRef                       sdRef,
   104                         DNSServiceFlags                     flags,
   105                         DNSServiceErrorType                 errorCode,
   106                         const char                          *name,
   107                         const char                          *regtype,
   108                         const char                          *domain,
   109                         void                                *context)
   110 {
   111     MYBonjourRegistration *reg = context;
   112     @try{
   113         if (!errorCode)
   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];
   119 }
   120 
   121 
   122 - (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
   123     DNSServiceFlags flags = 0;
   124     if (!_autoRename)
   125         flags |= kDNSServiceFlagsNoAutoRename;
   126     NSData *txtData = nil;
   127     if (_txtRecord)
   128         txtData = [NSNetService dataFromTXTRecordDictionary: _txtRecord];
   129     DNSServiceErrorType err;
   130     err = DNSServiceRegister(sdRefPtr,
   131                               flags,
   132                               0,
   133                               _name.UTF8String,         // _name is likely to be nil
   134                               _type.UTF8String,
   135                               _domain.UTF8String,       // _domain is most likely nil
   136                               NULL,
   137                               htons(_port),
   138                               txtData.length,
   139                               txtData.bytes,
   140                               &regCallback,
   141                               self);
   142     if (!err && _nullRecord)
   143         [self _updateNullRecord];
   144     return err;
   145 }
   146 
   147 
   148 - (void) cancel {
   149     if (_nullRecordReg && self.serviceRef) {
   150         DNSServiceRemoveRecord(self.serviceRef, _nullRecordReg, 0);
   151         _nullRecordReg = NULL;
   152     }
   153     
   154     [super cancel];
   155   
   156     if (_registered) {
   157         [[self class] priv_removeRegistration: self];
   158         self.registered = NO;
   159     }
   160 }
   161 
   162 
   163 + (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
   164     if (!txtDict)
   165         return nil;
   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];
   172         }
   173         [encodedDict setObject: value forKey: key];
   174     }
   175     return [NSNetService dataFromTXTRecordDictionary: encodedDict];
   176 }
   177 
   178 
   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));
   182     if (result==0) {
   183         if (length1>length2)
   184             result = 1;
   185         else if (length1<length2)
   186             result = -1;
   187     }
   188     return result;
   189 }
   190 
   191 + (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
   192 {
   193     if (!txtDict)
   194         return nil;
   195     
   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]);
   202                 return nil;
   203             }
   204             NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
   205             if (keyData.length > 255) {
   206                 Warn(@"TXT dictionary key too long: %@", key);
   207                 return nil;
   208             }
   209             id value = [txtDict objectForKey: key];
   210             if (![value isKindOfClass: [NSData class]]) {
   211                 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
   212             }
   213             if ([value length] > 255) {
   214                 Warn(@"TXT dictionary value too long: %@", value);
   215                 return nil;
   216             }
   217             [dataDict setObject: value forKey: keyData];
   218         }
   219     }
   220     
   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];
   233     }
   234     return canonical;
   235 }
   236 
   237 
   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,
   244                                                          NULL,
   245                                                          0,
   246                                                          data.length,
   247                                                          data.bytes,
   248                                                          kTXTTTL);
   249         if (err)
   250             Warn(@"%@ failed to update TXT (err=%i)", self,err);
   251         else
   252             LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
   253     }
   254 }
   255 
   256 
   257 - (NSDictionary*) txtRecord {
   258     return _txtRecord;
   259 }
   260 
   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];
   266     }
   267 }
   268 
   269 - (void) setString: (NSString*)value forTxtKey: (NSString*)key
   270 {
   271     NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
   272     if (!$equal(data, [_txtRecord objectForKey: key])) {
   273         if (data) {
   274             if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
   275             [_txtRecord setObject: data forKey: key];
   276         } else
   277             [_txtRecord removeObjectForKey: key];
   278         [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
   279         [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
   280     }
   281 }
   282 
   283 
   284 - (NSData*) nullRecord {
   285   return _nullRecord;
   286 }
   287 
   288 - (void) setNullRecord: (NSData*)nullRecord {
   289     if (ifSetObj(&_nullRecord, nullRecord))
   290         if (self.serviceRef)
   291             [self _updateNullRecord];
   292 }
   293 
   294 
   295 - (void) _updateNullRecord {
   296     DNSServiceRef serviceRef = self.serviceRef;
   297     Assert(serviceRef);
   298     DNSServiceErrorType err = 0;
   299     if (!_nullRecord) {
   300         if (_nullRecordReg) {
   301             err = DNSServiceRemoveRecord(serviceRef, _nullRecordReg, 0);
   302             _nullRecordReg = NULL;
   303         }
   304     } else if (!_nullRecordReg) {
   305         err = DNSServiceAddRecord(serviceRef, &_nullRecordReg, 0,
   306                                   kDNSServiceType_NULL, 
   307                                   _nullRecord.length, _nullRecord.bytes, 
   308                                   0);
   309     } else {
   310         err = DNSServiceUpdateRecord(serviceRef, _nullRecordReg, 0,
   311                                      _nullRecord.length, _nullRecord.bytes, 
   312                                      0);
   313     }
   314     if (err)
   315         Warn(@"MYBonjourRegistration: Couldn't update NULL record, err=%i",err);
   316     else
   317         LogTo(DNS, @"MYBonjourRegistration: Set NULL record (%u bytes) %@",
   318               _nullRecord.length, _nullRecord);
   319 }
   320 
   321 @end
   322 
   323 
   324 
   325 
   326 #pragma mark -
   327 #pragma mark TESTING:
   328 
   329 #if DEBUG
   330 
   331 #import "MYBonjourQuery.h"
   332 #import "MYAddressLookup.h"
   333 
   334 @interface BonjourRegTester : NSObject
   335 {
   336     MYBonjourRegistration *_reg;
   337     BOOL _updating;
   338 }
   339 @end
   340 
   341 @implementation BonjourRegTester
   342 
   343 - (void) updateTXT {
   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];
   348 }
   349 
   350 - (id) init
   351 {
   352     self = [super init];
   353     if (self != nil) {
   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];
   357         
   358         [self updateTXT];
   359         [_reg start];
   360     }
   361     return self;
   362 }
   363 
   364 - (void) dealloc
   365 {
   366     [_reg stop];
   367     [_reg removeObserver: self forKeyPath: @"registered"];
   368     [_reg removeObserver: self forKeyPath: @"name"];
   369     [_reg release];
   370     [super dealloc];
   371 }
   372 
   373 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
   374 {
   375     LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
   376 }
   377 
   378 @end
   379 
   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]];
   386     [tester release];
   387 }
   388 
   389 #endif
   390 
   391 
   392 /*
   393  Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   394  
   395  Redistribution and use in source and binary forms, with or without modification, are permitted
   396  provided that the following conditions are met:
   397  
   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
   402  distribution.
   403  
   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.
   412  */