Bonjour/MYBonjourRegistration.m
author Jens Alfke <jens@mooseyard.com>
Fri Jul 24 14:06:28 2009 -0700 (2009-07-24)
changeset 63 5e4855a592ee
parent 60 dd637bdd214e
permissions -rw-r--r--
* The BLIPConnection receivedRequest: delegate method now returns BOOL. If the method returns NO (or if the method isn't implemented in the delegate), that means it didn't handle the message at all; an error will be returned to the sender.
* If the connection closes unexpectedly due to an error, then the auto-generated responses to pending requests will contain that error. This makes it easier to display a meaningful error message in the handler for the request.
     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 #pragma mark 
   164 #pragma mark TXT RECORD:
   165 
   166 
   167 + (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
   168     if (!txtDict)
   169         return nil;
   170     // First translate any non-NSData values into UTF-8 formatted description data:
   171     NSMutableDictionary *encodedDict = $mdict();
   172     for (NSString *key in txtDict) {
   173         id value = [txtDict objectForKey: key];
   174         if (![value isKindOfClass: [NSData class]]) {
   175             value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
   176         }
   177         [encodedDict setObject: value forKey: key];
   178     }
   179     return [NSNetService dataFromTXTRecordDictionary: encodedDict];
   180 }
   181 
   182 
   183 static int compareData (id data1, id data2, void *context) {
   184     size_t length1 = [data1 length], length2 = [data2 length];
   185     int result = memcmp([data1 bytes], [data2 bytes], MIN(length1,length2));
   186     if (result==0) {
   187         if (length1>length2)
   188             result = 1;
   189         else if (length1<length2)
   190             result = -1;
   191     }
   192     return result;
   193 }
   194 
   195 + (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
   196 {
   197     if (!txtDict)
   198         return nil;
   199     
   200     // First convert keys and values to NSData:
   201     NSMutableDictionary *dataDict = $mdict();
   202     for (NSString *key in txtDict) {
   203         if (![key hasPrefix: @"("]) {               // ignore parenthesized keys
   204             if (![key isKindOfClass: [NSString class]]) {
   205                 Warn(@"TXT dictionary cannot have %@ as key", [key class]);
   206                 return nil;
   207             }
   208             NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
   209             if (keyData.length > 255) {
   210                 Warn(@"TXT dictionary key too long: %@", key);
   211                 return nil;
   212             }
   213             id value = [txtDict objectForKey: key];
   214             if (![value isKindOfClass: [NSData class]]) {
   215                 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
   216             }
   217             if ([value length] > 255) {
   218                 Warn(@"TXT dictionary value too long: %@", value);
   219                 return nil;
   220             }
   221             [dataDict setObject: value forKey: keyData];
   222         }
   223     }
   224     
   225     // Add key/value pairs, sorted by increasing key:
   226     NSMutableData *canonical = [NSMutableData dataWithCapacity: 1000];
   227     for (NSData *key in [[dataDict allKeys] sortedArrayUsingFunction: compareData context: NULL]) {
   228         // Append key prefixed with length:
   229         UInt8 length = [key length];
   230         [canonical appendBytes: &length length: sizeof(length)];
   231         [canonical appendData: key];
   232         // Append value prefixed with length:
   233         NSData *value = [dataDict objectForKey: key];
   234         length = [value length];
   235         [canonical appendBytes: &length length: sizeof(length)];
   236         [canonical appendData: value];
   237     }
   238     return canonical;
   239 }
   240 
   241 
   242 - (void) updateTXTRecord {
   243     [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTXTRecord) object: nil];
   244     if (self.serviceRef) {
   245         NSData *data = [[self class] dataFromTXTRecordDictionary: _txtRecord];
   246         Assert(data!=nil || _txtRecord==nil, @"Can't convert dictionary to TXT record: %@", _txtRecord);
   247         DNSServiceErrorType err = DNSServiceUpdateRecord(self.serviceRef,
   248                                                          NULL,
   249                                                          0,
   250                                                          data.length,
   251                                                          data.bytes,
   252                                                          kTXTTTL);
   253         if (err)
   254             Warn(@"%@ failed to update TXT (err=%i)", self,err);
   255         else
   256             LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
   257     }
   258 }
   259 
   260 
   261 - (NSDictionary*) TXTRecord {
   262     return _txtRecord;
   263 }
   264 
   265 - (void) setTXTRecord: (NSDictionary*)txtDict {
   266     if (!$equal(_txtRecord,txtDict)) {
   267         setObjCopy(&_txtRecord, txtDict);
   268         [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTXTRecord) object: nil];
   269         [self performSelector: @selector(updateTXTRecord) withObject: nil afterDelay: 0.1];
   270     }
   271 }
   272 
   273 - (void) setString: (NSString*)value forTXTKey: (NSString*)key
   274 {
   275     NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
   276     if (!$equal(data, [_txtRecord objectForKey: key])) {
   277         if (data) {
   278             if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
   279             [_txtRecord setObject: data forKey: key];
   280         } else
   281             [_txtRecord removeObjectForKey: key];
   282         [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTXTRecord) object: nil];
   283         [self performSelector: @selector(updateTXTRecord) withObject: nil afterDelay: 0.1];
   284     }
   285 }
   286 
   287 
   288 - (NSData*) nullRecord {
   289   return _nullRecord;
   290 }
   291 
   292 - (void) setNullRecord: (NSData*)nullRecord {
   293     if (ifSetObj(&_nullRecord, nullRecord))
   294         if (self.serviceRef)
   295             [self _updateNullRecord];
   296 }
   297 
   298 
   299 - (void) _updateNullRecord {
   300     DNSServiceRef serviceRef = self.serviceRef;
   301     Assert(serviceRef);
   302     DNSServiceErrorType err = 0;
   303     if (!_nullRecord) {
   304         if (_nullRecordReg) {
   305             err = DNSServiceRemoveRecord(serviceRef, _nullRecordReg, 0);
   306             _nullRecordReg = NULL;
   307         }
   308     } else if (!_nullRecordReg) {
   309         err = DNSServiceAddRecord(serviceRef, &_nullRecordReg, 0,
   310                                   kDNSServiceType_NULL, 
   311                                   _nullRecord.length, _nullRecord.bytes, 
   312                                   0);
   313     } else {
   314         err = DNSServiceUpdateRecord(serviceRef, _nullRecordReg, 0,
   315                                      _nullRecord.length, _nullRecord.bytes, 
   316                                      0);
   317     }
   318     if (err)
   319         Warn(@"MYBonjourRegistration: Couldn't update NULL record, err=%i",err);
   320     else
   321         LogTo(DNS, @"MYBonjourRegistration: Set NULL record (%u bytes) %@",
   322               _nullRecord.length, _nullRecord);
   323 }
   324 
   325 @end
   326 
   327 
   328 
   329 
   330 #pragma mark -
   331 #pragma mark TESTING:
   332 
   333 #if DEBUG
   334 
   335 #import "MYBonjourQuery.h"
   336 #import "MYAddressLookup.h"
   337 
   338 @interface BonjourRegTester : NSObject
   339 {
   340     MYBonjourRegistration *_reg;
   341     BOOL _updating;
   342 }
   343 @end
   344 
   345 @implementation BonjourRegTester
   346 
   347 - (void) updateTXT {
   348     NSDictionary *txt = $dict({@"time", $sprintf(@"%.3lf", CFAbsoluteTimeGetCurrent())});
   349     _reg.TXTRecord = txt;
   350     CAssertEqual(_reg.TXTRecord, txt);
   351     [self performSelector: @selector(updateTXT) withObject: nil afterDelay: 3.0];
   352 }
   353 
   354 - (id) init
   355 {
   356     self = [super init];
   357     if (self != nil) {
   358         _reg = [[MYBonjourRegistration alloc] initWithServiceType: @"_foo._tcp" port: 12345];
   359         [_reg addObserver: self forKeyPath: @"registered" options: NSKeyValueObservingOptionNew context: NULL];
   360         [_reg addObserver: self forKeyPath: @"name" options: NSKeyValueObservingOptionNew context: NULL];
   361         
   362         [self updateTXT];
   363         [_reg start];
   364     }
   365     return self;
   366 }
   367 
   368 - (void) dealloc
   369 {
   370     [_reg stop];
   371     [_reg removeObserver: self forKeyPath: @"registered"];
   372     [_reg removeObserver: self forKeyPath: @"name"];
   373     [_reg release];
   374     [super dealloc];
   375 }
   376 
   377 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
   378 {
   379     LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
   380 }
   381 
   382 @end
   383 
   384 TestCase(BonjourReg) {
   385     EnableLogTo(Bonjour,YES);
   386     EnableLogTo(DNS,YES);
   387     [NSRunLoop currentRunLoop]; // create runloop
   388     BonjourRegTester *tester = [[BonjourRegTester alloc] init];
   389     [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 15]];
   390     [tester release];
   391 }
   392 
   393 #endif
   394 
   395 
   396 /*
   397  Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   398  
   399  Redistribution and use in source and binary forms, with or without modification, are permitted
   400  provided that the following conditions are met:
   401  
   402  * Redistributions of source code must retain the above copyright notice, this list of conditions
   403  and the following disclaimer.
   404  * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
   405  and the following disclaimer in the documentation and/or other materials provided with the
   406  distribution.
   407  
   408  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
   409  IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
   410  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
   411  BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   412  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
   413   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
   414  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
   415  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   416  */