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.
jens@31
     1
//
jens@31
     2
//  MYBonjourRegistration.m
jens@31
     3
//  MYNetwork
jens@31
     4
//
jens@31
     5
//  Created by Jens Alfke on 4/27/09.
jens@31
     6
//  Copyright 2009 Jens Alfke. All rights reserved.
jens@31
     7
//
jens@31
     8
jens@31
     9
#import "MYBonjourRegistration.h"
jens@31
    10
#import "MYBonjourService.h"
jens@31
    11
#import "ExceptionUtils.h"
jens@31
    12
#import "Test.h"
jens@31
    13
#import "Logging.h"
jens@31
    14
#import <dns_sd.h>
jens@31
    15
jens@31
    16
jens@31
    17
#define kTXTTTL 60          // TTL in seconds for TXT records I register
jens@31
    18
jens@31
    19
jens@31
    20
@interface MYBonjourRegistration ()
jens@31
    21
@property BOOL registered;
jens@60
    22
- (void) _updateNullRecord;
jens@31
    23
@end
jens@31
    24
jens@31
    25
jens@31
    26
@implementation MYBonjourRegistration
jens@31
    27
jens@31
    28
jens@31
    29
static NSMutableDictionary *sAllRegistrations;
jens@31
    30
jens@31
    31
jens@31
    32
+ (void) priv_addRegistration: (MYBonjourRegistration*)reg {
jens@31
    33
    if (!sAllRegistrations)
jens@31
    34
        sAllRegistrations = [[NSMutableDictionary alloc] init];
jens@31
    35
    [sAllRegistrations setObject: reg forKey: reg.fullName];
jens@31
    36
}
jens@31
    37
jens@31
    38
+ (void) priv_removeRegistration: (MYBonjourRegistration*)reg {
jens@31
    39
    [sAllRegistrations removeObjectForKey: reg.fullName];
jens@31
    40
}
jens@31
    41
jens@31
    42
+ (MYBonjourRegistration*) registrationWithFullName: (NSString*)fullName {
jens@31
    43
    return [sAllRegistrations objectForKey: fullName];
jens@31
    44
}
jens@31
    45
jens@31
    46
jens@31
    47
- (id) initWithServiceType: (NSString*)serviceType port: (UInt16)port
jens@31
    48
{
jens@31
    49
    self = [super init];
jens@31
    50
    if (self != nil) {
jens@31
    51
        self.continuous = YES;
jens@31
    52
        self.usePrivateConnection = YES;    // DNSServiceUpdateRecord doesn't work with shared conn :(
jens@31
    53
        _type = [serviceType copy];
jens@31
    54
        _port = port;
jens@31
    55
        _autoRename = YES;
jens@31
    56
    }
jens@31
    57
    return self;
jens@31
    58
}
jens@31
    59
jens@31
    60
- (void) dealloc {
jens@31
    61
    [_name release];
jens@31
    62
    [_type release];
jens@31
    63
    [_domain release];
jens@50
    64
    [_txtRecord release];
jens@31
    65
    [super dealloc];
jens@31
    66
}
jens@31
    67
jens@31
    68
jens@31
    69
@synthesize name=_name, type=_type, domain=_domain, port=_port, autoRename=_autoRename;
jens@31
    70
@synthesize registered=_registered;
jens@31
    71
jens@31
    72
jens@31
    73
- (NSString*) fullName {
jens@31
    74
    return [[self class] fullNameOfService: _name ofType: _type inDomain: _domain];
jens@31
    75
}
jens@31
    76
jens@31
    77
jens@31
    78
- (BOOL) isSameAsService: (MYBonjourService*)service {
jens@31
    79
    return _name && _domain && [self.fullName isEqualToString: service.fullName];
jens@31
    80
}
jens@31
    81
jens@31
    82
jens@31
    83
- (NSString*) description
jens@31
    84
{
jens@31
    85
    return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
jens@31
    86
}
jens@31
    87
jens@31
    88
jens@31
    89
- (void) priv_registeredAsName: (NSString*)name 
jens@31
    90
                          type: (NSString*)regtype
jens@31
    91
                        domain: (NSString*)domain
jens@31
    92
{
jens@31
    93
    if (!$equal(name,_name))
jens@31
    94
        self.name = name;
jens@31
    95
    if (!$equal(domain,_domain))
jens@31
    96
        self.domain = domain;
jens@31
    97
    LogTo(Bonjour,@"Registered %@", self);
jens@31
    98
    self.registered = YES;
jens@31
    99
    [[self class] priv_addRegistration: self];
jens@31
   100
}
jens@31
   101
jens@31
   102
jens@31
   103
static void regCallback(DNSServiceRef                       sdRef,
jens@31
   104
                        DNSServiceFlags                     flags,
jens@31
   105
                        DNSServiceErrorType                 errorCode,
jens@31
   106
                        const char                          *name,
jens@31
   107
                        const char                          *regtype,
jens@31
   108
                        const char                          *domain,
jens@31
   109
                        void                                *context)
jens@31
   110
{
jens@31
   111
    MYBonjourRegistration *reg = context;
jens@31
   112
    @try{
jens@31
   113
        if (!errorCode)
jens@31
   114
            [reg priv_registeredAsName: [NSString stringWithUTF8String: name]
jens@31
   115
                                  type: [NSString stringWithUTF8String: regtype]
jens@31
   116
                                domain: [NSString stringWithUTF8String: domain]];
jens@31
   117
    }catchAndReport(@"MYBonjourRegistration callback");
jens@31
   118
    [reg gotResponse: errorCode];
jens@31
   119
}
jens@31
   120
jens@31
   121
jens@31
   122
- (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
jens@31
   123
    DNSServiceFlags flags = 0;
jens@31
   124
    if (!_autoRename)
jens@31
   125
        flags |= kDNSServiceFlagsNoAutoRename;
jens@31
   126
    NSData *txtData = nil;
jens@31
   127
    if (_txtRecord)
jens@31
   128
        txtData = [NSNetService dataFromTXTRecordDictionary: _txtRecord];
jens@60
   129
    DNSServiceErrorType err;
jens@60
   130
    err = DNSServiceRegister(sdRefPtr,
jens@31
   131
                              flags,
jens@31
   132
                              0,
jens@31
   133
                              _name.UTF8String,         // _name is likely to be nil
jens@31
   134
                              _type.UTF8String,
jens@31
   135
                              _domain.UTF8String,       // _domain is most likely nil
jens@31
   136
                              NULL,
jens@31
   137
                              htons(_port),
jens@31
   138
                              txtData.length,
jens@31
   139
                              txtData.bytes,
jens@31
   140
                              &regCallback,
jens@31
   141
                              self);
jens@60
   142
    if (!err && _nullRecord)
jens@60
   143
        [self _updateNullRecord];
jens@60
   144
    return err;
jens@31
   145
}
jens@31
   146
jens@31
   147
jens@31
   148
- (void) cancel {
jens@60
   149
    if (_nullRecordReg && self.serviceRef) {
jens@60
   150
        DNSServiceRemoveRecord(self.serviceRef, _nullRecordReg, 0);
jens@60
   151
        _nullRecordReg = NULL;
jens@60
   152
    }
jens@60
   153
    
jens@31
   154
    [super cancel];
jens@60
   155
  
jens@31
   156
    if (_registered) {
jens@31
   157
        [[self class] priv_removeRegistration: self];
jens@31
   158
        self.registered = NO;
jens@31
   159
    }
jens@31
   160
}
jens@31
   161
jens@31
   162
jens@47
   163
+ (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
jens@50
   164
    if (!txtDict)
jens@50
   165
        return nil;
jens@47
   166
    // First translate any non-NSData values into UTF-8 formatted description data:
jens@47
   167
    NSMutableDictionary *encodedDict = $mdict();
jens@47
   168
    for (NSString *key in txtDict) {
jens@47
   169
        id value = [txtDict objectForKey: key];
jens@47
   170
        if (![value isKindOfClass: [NSData class]]) {
jens@47
   171
            value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
jens@47
   172
        }
jens@47
   173
        [encodedDict setObject: value forKey: key];
jens@47
   174
    }
jens@47
   175
    return [NSNetService dataFromTXTRecordDictionary: encodedDict];
jens@47
   176
}
jens@47
   177
jens@47
   178
jens@59
   179
static int compareData (id data1, id data2, void *context) {
jens@59
   180
    size_t length1 = [data1 length], length2 = [data2 length];
jens@59
   181
    int result = memcmp([data1 bytes], [data2 bytes], MIN(length1,length2));
jens@59
   182
    if (result==0) {
jens@59
   183
        if (length1>length2)
jens@59
   184
            result = 1;
jens@59
   185
        else if (length1<length2)
jens@59
   186
            result = -1;
jens@59
   187
    }
jens@59
   188
    return result;
jens@59
   189
}
jens@59
   190
jens@59
   191
+ (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
jens@59
   192
{
jens@59
   193
    if (!txtDict)
jens@59
   194
        return nil;
jens@59
   195
    
jens@59
   196
    // First convert keys and values to NSData:
jens@59
   197
    NSMutableDictionary *dataDict = $mdict();
jens@59
   198
    for (NSString *key in txtDict) {
jens@59
   199
        if (![key hasPrefix: @"("]) {               // ignore parenthesized keys
jens@59
   200
            if (![key isKindOfClass: [NSString class]]) {
jens@59
   201
                Warn(@"TXT dictionary cannot have %@ as key", [key class]);
jens@59
   202
                return nil;
jens@59
   203
            }
jens@59
   204
            NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
jens@59
   205
            if (keyData.length > 255) {
jens@59
   206
                Warn(@"TXT dictionary key too long: %@", key);
jens@59
   207
                return nil;
jens@59
   208
            }
jens@59
   209
            id value = [txtDict objectForKey: key];
jens@59
   210
            if (![value isKindOfClass: [NSData class]]) {
jens@59
   211
                value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
jens@59
   212
            }
jens@59
   213
            if ([value length] > 255) {
jens@59
   214
                Warn(@"TXT dictionary value too long: %@", value);
jens@59
   215
                return nil;
jens@59
   216
            }
jens@59
   217
            [dataDict setObject: value forKey: keyData];
jens@59
   218
        }
jens@59
   219
    }
jens@59
   220
    
jens@59
   221
    // Add key/value pairs, sorted by increasing key:
jens@59
   222
    NSMutableData *canonical = [NSMutableData dataWithCapacity: 1000];
jens@59
   223
    for (NSData *key in [[dataDict allKeys] sortedArrayUsingFunction: compareData context: NULL]) {
jens@59
   224
        // Append key prefixed with length:
jens@59
   225
        UInt8 length = [key length];
jens@59
   226
        [canonical appendBytes: &length length: sizeof(length)];
jens@59
   227
        [canonical appendData: key];
jens@59
   228
        // Append value prefixed with length:
jens@59
   229
        NSData *value = [dataDict objectForKey: key];
jens@59
   230
        length = [value length];
jens@59
   231
        [canonical appendBytes: &length length: sizeof(length)];
jens@59
   232
        [canonical appendData: value];
jens@59
   233
    }
jens@59
   234
    return canonical;
jens@59
   235
}
jens@59
   236
jens@59
   237
jens@31
   238
- (void) updateTxtRecord {
jens@31
   239
    [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
jens@31
   240
    if (self.serviceRef) {
jens@47
   241
        NSData *data = [[self class] dataFromTXTRecordDictionary: _txtRecord];
jens@50
   242
        Assert(data!=nil || _txtRecord==nil, @"Can't convert dictionary to TXT record: %@", _txtRecord);
jens@31
   243
        DNSServiceErrorType err = DNSServiceUpdateRecord(self.serviceRef,
jens@31
   244
                                                         NULL,
jens@31
   245
                                                         0,
jens@31
   246
                                                         data.length,
jens@31
   247
                                                         data.bytes,
jens@31
   248
                                                         kTXTTTL);
jens@31
   249
        if (err)
jens@31
   250
            Warn(@"%@ failed to update TXT (err=%i)", self,err);
jens@31
   251
        else
jens@59
   252
            LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
jens@31
   253
    }
jens@31
   254
}
jens@31
   255
jens@31
   256
jens@31
   257
- (NSDictionary*) txtRecord {
jens@31
   258
    return _txtRecord;
jens@31
   259
}
jens@31
   260
jens@31
   261
- (void) setTxtRecord: (NSDictionary*)txtDict {
jens@31
   262
    if (!$equal(_txtRecord,txtDict)) {
jens@50
   263
        setObjCopy(&_txtRecord, txtDict);
jens@31
   264
        [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
jens@31
   265
        [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
jens@31
   266
    }
jens@31
   267
}
jens@31
   268
jens@31
   269
- (void) setString: (NSString*)value forTxtKey: (NSString*)key
jens@31
   270
{
jens@31
   271
    NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
jens@31
   272
    if (!$equal(data, [_txtRecord objectForKey: key])) {
jens@31
   273
        if (data) {
jens@31
   274
            if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
jens@31
   275
            [_txtRecord setObject: data forKey: key];
jens@31
   276
        } else
jens@31
   277
            [_txtRecord removeObjectForKey: key];
jens@31
   278
        [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTxtRecord) object: nil];
jens@31
   279
        [self performSelector: @selector(updateTxtRecord) withObject: nil afterDelay: 0.1];
jens@31
   280
    }
jens@31
   281
}
jens@31
   282
jens@60
   283
jens@60
   284
- (NSData*) nullRecord {
jens@60
   285
  return _nullRecord;
jens@60
   286
}
jens@60
   287
jens@60
   288
- (void) setNullRecord: (NSData*)nullRecord {
jens@60
   289
    if (ifSetObj(&_nullRecord, nullRecord))
jens@60
   290
        if (self.serviceRef)
jens@60
   291
            [self _updateNullRecord];
jens@60
   292
}
jens@60
   293
jens@60
   294
jens@60
   295
- (void) _updateNullRecord {
jens@60
   296
    DNSServiceRef serviceRef = self.serviceRef;
jens@60
   297
    Assert(serviceRef);
jens@60
   298
    DNSServiceErrorType err = 0;
jens@60
   299
    if (!_nullRecord) {
jens@60
   300
        if (_nullRecordReg) {
jens@60
   301
            err = DNSServiceRemoveRecord(serviceRef, _nullRecordReg, 0);
jens@60
   302
            _nullRecordReg = NULL;
jens@60
   303
        }
jens@60
   304
    } else if (!_nullRecordReg) {
jens@60
   305
        err = DNSServiceAddRecord(serviceRef, &_nullRecordReg, 0,
jens@60
   306
                                  kDNSServiceType_NULL, 
jens@60
   307
                                  _nullRecord.length, _nullRecord.bytes, 
jens@60
   308
                                  0);
jens@60
   309
    } else {
jens@60
   310
        err = DNSServiceUpdateRecord(serviceRef, _nullRecordReg, 0,
jens@60
   311
                                     _nullRecord.length, _nullRecord.bytes, 
jens@60
   312
                                     0);
jens@60
   313
    }
jens@60
   314
    if (err)
jens@60
   315
        Warn(@"MYBonjourRegistration: Couldn't update NULL record, err=%i",err);
jens@60
   316
    else
jens@60
   317
        LogTo(DNS, @"MYBonjourRegistration: Set NULL record (%u bytes) %@",
jens@60
   318
              _nullRecord.length, _nullRecord);
jens@60
   319
}
jens@60
   320
jens@31
   321
@end
jens@31
   322
jens@31
   323
jens@31
   324
jens@31
   325
jens@31
   326
#pragma mark -
jens@31
   327
#pragma mark TESTING:
jens@31
   328
jens@31
   329
#if DEBUG
jens@31
   330
jens@31
   331
#import "MYBonjourQuery.h"
jens@31
   332
#import "MYAddressLookup.h"
jens@31
   333
jens@31
   334
@interface BonjourRegTester : NSObject
jens@31
   335
{
jens@31
   336
    MYBonjourRegistration *_reg;
jens@31
   337
    BOOL _updating;
jens@31
   338
}
jens@31
   339
@end
jens@31
   340
jens@31
   341
@implementation BonjourRegTester
jens@31
   342
jens@31
   343
- (void) updateTXT {
jens@31
   344
    NSDictionary *txt = $dict({@"time", $sprintf(@"%.3lf", CFAbsoluteTimeGetCurrent())});
jens@31
   345
    _reg.txtRecord = txt;
jens@50
   346
    CAssertEqual(_reg.txtRecord, txt);
jens@31
   347
    [self performSelector: @selector(updateTXT) withObject: nil afterDelay: 3.0];
jens@31
   348
}
jens@31
   349
jens@31
   350
- (id) init
jens@31
   351
{
jens@31
   352
    self = [super init];
jens@31
   353
    if (self != nil) {
jens@31
   354
        _reg = [[MYBonjourRegistration alloc] initWithServiceType: @"_foo._tcp" port: 12345];
jens@31
   355
        [_reg addObserver: self forKeyPath: @"registered" options: NSKeyValueObservingOptionNew context: NULL];
jens@31
   356
        [_reg addObserver: self forKeyPath: @"name" options: NSKeyValueObservingOptionNew context: NULL];
jens@31
   357
        
jens@31
   358
        [self updateTXT];
jens@31
   359
        [_reg start];
jens@31
   360
    }
jens@31
   361
    return self;
jens@31
   362
}
jens@31
   363
jens@31
   364
- (void) dealloc
jens@31
   365
{
jens@31
   366
    [_reg stop];
jens@31
   367
    [_reg removeObserver: self forKeyPath: @"registered"];
jens@31
   368
    [_reg removeObserver: self forKeyPath: @"name"];
jens@31
   369
    [_reg release];
jens@31
   370
    [super dealloc];
jens@31
   371
}
jens@31
   372
jens@31
   373
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
jens@31
   374
{
jens@31
   375
    LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
jens@31
   376
}
jens@31
   377
jens@31
   378
@end
jens@31
   379
jens@31
   380
TestCase(BonjourReg) {
jens@31
   381
    EnableLogTo(Bonjour,YES);
jens@31
   382
    EnableLogTo(DNS,YES);
jens@31
   383
    [NSRunLoop currentRunLoop]; // create runloop
jens@31
   384
    BonjourRegTester *tester = [[BonjourRegTester alloc] init];
jens@31
   385
    [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 15]];
jens@31
   386
    [tester release];
jens@31
   387
}
jens@31
   388
jens@31
   389
#endif
jens@31
   390
jens@31
   391
jens@31
   392
/*
jens@31
   393
 Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
jens@31
   394
 
jens@31
   395
 Redistribution and use in source and binary forms, with or without modification, are permitted
jens@31
   396
 provided that the following conditions are met:
jens@31
   397
 
jens@31
   398
 * Redistributions of source code must retain the above copyright notice, this list of conditions
jens@31
   399
 and the following disclaimer.
jens@31
   400
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
jens@31
   401
 and the following disclaimer in the documentation and/or other materials provided with the
jens@31
   402
 distribution.
jens@31
   403
 
jens@31
   404
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
jens@31
   405
 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
jens@31
   406
 FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI-
jens@31
   407
 BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
jens@31
   408
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
jens@31
   409
  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
jens@31
   410
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
jens@31
   411
 THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
jens@31
   412
 */