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