* 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.
2 // MYBonjourRegistration.m
5 // Created by Jens Alfke on 4/27/09.
6 // Copyright 2009 Jens Alfke. All rights reserved.
9 #import "MYBonjourRegistration.h"
10 #import "MYBonjourService.h"
11 #import "ExceptionUtils.h"
17 #define kTXTTTL 60 // TTL in seconds for TXT records I register
20 @interface MYBonjourRegistration ()
21 @property BOOL registered;
22 - (void) _updateNullRecord;
26 @implementation MYBonjourRegistration
29 static NSMutableDictionary *sAllRegistrations;
32 + (void) priv_addRegistration: (MYBonjourRegistration*)reg {
33 if (!sAllRegistrations)
34 sAllRegistrations = [[NSMutableDictionary alloc] init];
35 [sAllRegistrations setObject: reg forKey: reg.fullName];
38 + (void) priv_removeRegistration: (MYBonjourRegistration*)reg {
39 [sAllRegistrations removeObjectForKey: reg.fullName];
42 + (MYBonjourRegistration*) registrationWithFullName: (NSString*)fullName {
43 return [sAllRegistrations objectForKey: fullName];
47 - (id) initWithServiceType: (NSString*)serviceType port: (UInt16)port
51 self.continuous = YES;
52 self.usePrivateConnection = YES; // DNSServiceUpdateRecord doesn't work with shared conn :(
53 _type = [serviceType copy];
69 @synthesize name=_name, type=_type, domain=_domain, port=_port, autoRename=_autoRename;
70 @synthesize registered=_registered;
73 - (NSString*) fullName {
74 return [[self class] fullNameOfService: _name ofType: _type inDomain: _domain];
78 - (BOOL) isSameAsService: (MYBonjourService*)service {
79 return _name && _domain && [self.fullName isEqualToString: service.fullName];
83 - (NSString*) description
85 return $sprintf(@"%@['%@'.%@%@]", self.class,_name,_type,_domain);
89 - (void) priv_registeredAsName: (NSString*)name
90 type: (NSString*)regtype
91 domain: (NSString*)domain
93 if (!$equal(name,_name))
95 if (!$equal(domain,_domain))
97 LogTo(Bonjour,@"Registered %@", self);
98 self.registered = YES;
99 [[self class] priv_addRegistration: self];
103 static void regCallback(DNSServiceRef sdRef,
104 DNSServiceFlags flags,
105 DNSServiceErrorType errorCode,
111 MYBonjourRegistration *reg = context;
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];
122 - (DNSServiceErrorType) createServiceRef: (DNSServiceRef*)sdRefPtr {
123 DNSServiceFlags flags = 0;
125 flags |= kDNSServiceFlagsNoAutoRename;
126 NSData *txtData = nil;
128 txtData = [NSNetService dataFromTXTRecordDictionary: _txtRecord];
129 DNSServiceErrorType err;
130 err = DNSServiceRegister(sdRefPtr,
133 _name.UTF8String, // _name is likely to be nil
135 _domain.UTF8String, // _domain is most likely nil
142 if (!err && _nullRecord)
143 [self _updateNullRecord];
149 if (_nullRecordReg && self.serviceRef) {
150 DNSServiceRemoveRecord(self.serviceRef, _nullRecordReg, 0);
151 _nullRecordReg = NULL;
157 [[self class] priv_removeRegistration: self];
158 self.registered = NO;
164 #pragma mark TXT RECORD:
167 + (NSData*) dataFromTXTRecordDictionary: (NSDictionary*)txtDict {
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];
177 [encodedDict setObject: value forKey: key];
179 return [NSNetService dataFromTXTRecordDictionary: encodedDict];
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));
189 else if (length1<length2)
195 + (NSData*) canonicalFormOfTXTRecordDictionary: (NSDictionary*)txtDict
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]);
208 NSData *keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
209 if (keyData.length > 255) {
210 Warn(@"TXT dictionary key too long: %@", key);
213 id value = [txtDict objectForKey: key];
214 if (![value isKindOfClass: [NSData class]]) {
215 value = [[value description] dataUsingEncoding: NSUTF8StringEncoding];
217 if ([value length] > 255) {
218 Warn(@"TXT dictionary value too long: %@", value);
221 [dataDict setObject: value forKey: keyData];
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];
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,
254 Warn(@"%@ failed to update TXT (err=%i)", self,err);
256 LogTo(Bonjour,@"%@ updated TXT to %u bytes: %@", self,data.length,data);
261 - (NSDictionary*) TXTRecord {
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];
273 - (void) setString: (NSString*)value forTXTKey: (NSString*)key
275 NSData *data = [value dataUsingEncoding: NSUTF8StringEncoding];
276 if (!$equal(data, [_txtRecord objectForKey: key])) {
278 if (!_txtRecord) _txtRecord = [[NSMutableDictionary alloc] init];
279 [_txtRecord setObject: data forKey: key];
281 [_txtRecord removeObjectForKey: key];
282 [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @selector(updateTXTRecord) object: nil];
283 [self performSelector: @selector(updateTXTRecord) withObject: nil afterDelay: 0.1];
288 - (NSData*) nullRecord {
292 - (void) setNullRecord: (NSData*)nullRecord {
293 if (ifSetObj(&_nullRecord, nullRecord))
295 [self _updateNullRecord];
299 - (void) _updateNullRecord {
300 DNSServiceRef serviceRef = self.serviceRef;
302 DNSServiceErrorType err = 0;
304 if (_nullRecordReg) {
305 err = DNSServiceRemoveRecord(serviceRef, _nullRecordReg, 0);
306 _nullRecordReg = NULL;
308 } else if (!_nullRecordReg) {
309 err = DNSServiceAddRecord(serviceRef, &_nullRecordReg, 0,
310 kDNSServiceType_NULL,
311 _nullRecord.length, _nullRecord.bytes,
314 err = DNSServiceUpdateRecord(serviceRef, _nullRecordReg, 0,
315 _nullRecord.length, _nullRecord.bytes,
319 Warn(@"MYBonjourRegistration: Couldn't update NULL record, err=%i",err);
321 LogTo(DNS, @"MYBonjourRegistration: Set NULL record (%u bytes) %@",
322 _nullRecord.length, _nullRecord);
331 #pragma mark TESTING:
335 #import "MYBonjourQuery.h"
336 #import "MYAddressLookup.h"
338 @interface BonjourRegTester : NSObject
340 MYBonjourRegistration *_reg;
345 @implementation BonjourRegTester
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];
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];
371 [_reg removeObserver: self forKeyPath: @"registered"];
372 [_reg removeObserver: self forKeyPath: @"name"];
377 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
379 LogTo(Bonjour,@"Observed change in %@: %@",keyPath,change);
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]];
397 Copyright (c) 2008-2009, Jens Alfke <jens@mooseyard.com>. All rights reserved.
399 Redistribution and use in source and binary forms, with or without modification, are permitted
400 provided that the following conditions are met:
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
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.