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 |
®Callback,
|
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 |
*/
|