Bonjour/MYBonjourRegistration.m
author Jens Alfke <jens@mooseyard.com>
Mon Jul 20 13:26:29 2009 -0700 (2009-07-20)
changeset 59 46c7844cb592
parent 50 63baa74c903f
child 60 dd637bdd214e
permissions -rw-r--r--
* 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.
     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 @end
    23 
    24 
    25 @implementation MYBonjourRegistration
    26 
    27 
    28 static NSMutableDictionary *sAllRegistrations;
    29 
    30 
    31 + (void) priv_addRegistration: (MYBonjourRegistration*)reg {
    32     if (!sAllRegistrations)
    33         sAllRegistrations = [[NSMutableDictionary alloc] init];
    34     [sAllRegistrations setObject: reg forKey: reg.fullName];
    35 }
    36 
    37 + (void) priv_removeRegistration: (MYBonjourRegistration*)reg {
    38     [sAllRegistrations removeObjectForKey: reg.fullName];
    39 }
    40 
    41 + (MYBonjourRegistration*) registrationWithFullName: (NSString*)fullName {
    42     return [sAllRegistrations objectForKey: fullName];
    43 }
    44 
    45 
    46 - (id) initWithServiceType: (NSString*)serviceType port: (UInt16)port
    47 {
    48     self = [super init];
    49     if (self != nil) {
    50         self.continuous = YES;
    51         self.usePrivateConnection = YES;    // DNSServiceUpdateRecord doesn't work with shared conn :(
    52         _type = [serviceType copy];
    53         _port = port;
    54         _autoRename = YES;
    55     }
    56     return self;
    57 }
    58 
    59 - (void) dealloc {
    60     [_name release];
    61     [_type release];
    62     [_domain release];
    63     [_txtRecord release];
    64     [super dealloc];
    65 }
    66 
    67 
    68 @synthesize name=_name, type=_type, domain=_domain, port=_port, autoRename=_autoRename;
    69 @synthesize registered=_registered;
    70 
    71 
    72 - (NSString*) fullName {
    73     return [[self class] fullNameOfService: _name ofType: _type inDomain: _domain];
    74 }
    75 
    76 
    77 - (BOOL) isSameAsService: (MYBonjourService*)service {
    78     return _name && _domain && [self.fullName isEqualToString: service.fullName];
    79 }
    80 
    81 
    82 - (NSString*) description
    83 {
    84     return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
    85 }
    86 
    87 
    88 - (void) priv_registeredAsName: (NSString*)name 
    89                           type: (NSString*)regtype
    90                         domain: (NSString*)domain
    91 {
    92     if (!$equal(name,_name))
    93         self.name = name;
    94     if (!$equal(domain,_domain))
    95         self.domain = domain;
    96     LogTo(Bonjour,@"Registered %@", self);
    97     self.registered = YES;
    98     [[self class] priv_addRegistration: self];
    99 }
   100 
   101 
   102 static void regCallback(DNSServiceRef                       sdRef,
   103                         DNSServiceFlags                     flags,
   104                         DNSServiceErrorType                 errorCode,
   105                         const char                          *name,
   106                         const char                          *regtype,
   107                         const char                          *domain,
   108                         void                                *context)
   109 {
   110     MYBonjourRegistration *reg = context;
   111     @try{
   112         if (!errorCode)
   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];
   118 }
   119 
   120 
   121 - (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
   122     DNSServiceFlags flags = 0;
   123     if (!_autoRename)
   124         flags |= kDNSServiceFlagsNoAutoRename;
   125     NSData *txtData = nil;
   126     if (_txtRecord)
   127         txtData = [NSNetService dataFromTXTRecordDictionary: _txtRecord];
   128     return DNSServiceRegister(sdRefPtr,
   129                               flags,
   130                               0,
   131                               _name.UTF8String,         // _name is likely to be nil
   132                               _type.UTF8String,
   133                               _domain.UTF8String,       // _domain is most likely nil
   134                               NULL,
   135                               htons(_port),
   136                               txtData.length,
   137                               txtData.bytes,
   138                               &regCallback,
   139                               self);
   140 }
   141 
   142 
   143 - (void) cancel {
   144     [super cancel];
   145     if (_registered) {
   146         [[self class] priv_removeRegistration: self];
   147         self.registered = NO;
   148     }
   149 }
   150 
   151 
   152 + (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
   153     if (!txtDict)
   154         return nil;
   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];
   161         }
   162         [encodedDict setObject: value forKey: key];
   163     }
   164     return [NSNetService dataFromTXTRecordDictionary: encodedDict];
   165 }
   166 
   167 
   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));
   171     if (result==0) {
   172         if (length1>length2)
   173             result = 1;
   174         else if (length1<length2)
   175             result = -1;
   176     }
   177     return result;
   178 }
   179 
   180 + (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
   181 {
   182     if (!txtDict)
   183         return nil;
   184     
   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]);
   191                 return nil;
   192             }
   193             NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
   194             if (keyData.length > 255) {
   195                 Warn(@"TXT dictionary key too long: %@", key);
   196                 return nil;
   197             }
   198             id value = [txtDict objectForKey: key];
   199             if (![value isKindOfClass: [NSData class]]) {
   200                 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
   201             }
   202             if ([value length] > 255) {
   203                 Warn(@"TXT dictionary value too long: %@", value);
   204                 return nil;
   205             }
   206             [dataDict setObject: value forKey: keyData];
   207         }
   208     }
   209     
   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];
   222     }
   223     return canonical;
   224 }
   225 
   226 
   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,
   233                                                          NULL,
   234                                                          0,
   235                                                          data.length,
   236                                                          data.bytes,
   237                                                          kTXTTTL);
   238         if (err)
   239             Warn(@"%@ failed to update TXT (err=%i)", self,err);
   240         else
   241             LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
   242     }
   243 }
   244 
   245 
   246 - (NSDictionary*) txtRecord {
   247     return _txtRecord;
   248 }
   249 
   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];
   255     }
   256 }
   257 
   258 - (void) setString: (NSString*)value forTxtKey: (NSString*)key
   259 {
   260     NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
   261     if (!$equal(data, [_txtRecord objectForKey: key])) {
   262         if (data) {
   263             if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
   264             [_txtRecord setObject: data forKey: key];
   265         } else
   266             [_txtRecord removeObjectForKey: key];
   267         [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
   268         [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
   269     }
   270 }
   271 
   272 @end
   273 
   274 
   275 
   276 
   277 #pragma mark -
   278 #pragma mark TESTING:
   279 
   280 #if DEBUG
   281 
   282 #import "MYBonjourQuery.h"
   283 #import "MYAddressLookup.h"
   284 
   285 @interface BonjourRegTester : NSObject
   286 {
   287     MYBonjourRegistration *_reg;
   288     BOOL _updating;
   289 }
   290 @end
   291 
   292 @implementation BonjourRegTester
   293 
   294 - (void) updateTXT {
   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];
   299 }
   300 
   301 - (id) init
   302 {
   303     self = [super init];
   304     if (self != nil) {
   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];
   308         
   309         [self updateTXT];
   310         [_reg start];
   311     }
   312     return self;
   313 }
   314 
   315 - (void) dealloc
   316 {
   317     [_reg stop];
   318     [_reg removeObserver: self forKeyPath: @"registered"];
   319     [_reg removeObserver: self forKeyPath: @"name"];
   320     [_reg release];
   321     [super dealloc];
   322 }
   323 
   324 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
   325 {
   326     LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
   327 }
   328 
   329 @end
   330 
   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]];
   337     [tester release];
   338 }
   339 
   340 #endif
   341 
   342 
   343 /*
   344  Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
   345  
   346  Redistribution and use in source and binary forms, with or without modification, are permitted
   347  provided that the following conditions are met:
   348  
   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
   353  distribution.
   354  
   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.
   363  */