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