mirror of
https://github.com/gnustep/libs-base.git
synced 2025-04-22 08:26:27 +00:00
2048 lines
54 KiB
Objective-C
2048 lines
54 KiB
Objective-C
/* Concrete NSNetService subclass using the avahi API.
|
|
Copyright (C) 2010 Free Software Foundation, Inc.
|
|
|
|
Written by: Niels Grewe <niels.grewe@halbordnung.de>
|
|
Date: March 2010
|
|
|
|
This file is part of the GNUstep Base Library.
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free
|
|
Software Foundation, Inc., 31 Milk Street #960789 Boston, MA 02196 USA.
|
|
*/
|
|
|
|
#import "GSNetServices.h"
|
|
#import "GSAvahiClient.h"
|
|
#import "GNUstepBase/NSNetServices+GNUstepBase.h"
|
|
#import "Foundation/NSDictionary.h"
|
|
#import "Foundation/NSData.h"
|
|
#import "Foundation/NSValue.h"
|
|
#import "Foundation/NSMapTable.h"
|
|
#import "Foundation/NSDebug.h"
|
|
#import "Foundation/NSLock.h"
|
|
#import "Foundation/NSException.h"
|
|
#import "GNUstepBase/GSObjCRuntime.h"
|
|
#import <GNUstepBase/NSStream+GNUstepBase.h>
|
|
#include <avahi-common/error.h>
|
|
#include <avahi-common/malloc.h>
|
|
#include <avahi-common/strlst.h>
|
|
#include <avahi-common/alternative.h>
|
|
#include <avahi-client/lookup.h>
|
|
#include <avahi-client/publish.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
|
|
/* Specifies the time we allow for consecutive updates on a specific record type
|
|
* to accumulate before the delegate is notified. Avahi seems to run some
|
|
* internal timers set to one second, so we choose a slightly greater interval
|
|
* to be sure that they did already fire in a simple remove/insert sequence.
|
|
*/
|
|
#define BROWSER_UPDATE_INTERVAL 1.1
|
|
|
|
/* Suggested by the mDNS RFC, but we only use the latter since this is
|
|
* NSNet_Service_ and not NSNet_Host_!
|
|
*/
|
|
#define DEFAULT_HOST_RECORD_TTL 120
|
|
#define DEFAULT_OTHER_RECORD_TTL 4500
|
|
|
|
|
|
@interface GSAvahiNetService (GSAvahiNetServicePrivate)
|
|
- (void) newData: (NSData*)data
|
|
forRRCode: (NSUInteger)rrCode;
|
|
|
|
- (void) removedData: (NSData*)data
|
|
forRRCode: (NSUInteger)rrCode;
|
|
|
|
- (void) allForNowForRRCode: (NSUInteger)rrCode;
|
|
|
|
- (void) entryGroup: (AvahiEntryGroup*)group
|
|
enteredState: (AvahiEntryGroupState)state;
|
|
|
|
- (void) avahiResolver: (AvahiServiceResolver*)resolver
|
|
foundServiceWithName: (NSString*)name
|
|
type: (NSString*)type
|
|
domain: (NSString*)domain
|
|
hostName: (NSString*)hostName
|
|
address: (NSData*)address
|
|
port: (NSInteger)port
|
|
txtRecord: (NSData*)txtData
|
|
ifIndex: (AvahiIfIndex)ifIndex
|
|
protocol: (AvahiProtocol)protocol;
|
|
|
|
- (void) handleError: (int)error
|
|
forRRCode: (int)rrcode;
|
|
|
|
- (void) handleError: (int)error;
|
|
|
|
|
|
// Methods from the GSAvahiClient behaviour:
|
|
|
|
- (id) avahiClientInit;
|
|
|
|
- (void) avahiClientHandleError: (int)error;
|
|
|
|
- (void) avahiClientDealloc;
|
|
|
|
- (NSDictionary*) errorDictWithErrorCode: (int)error;
|
|
@end
|
|
|
|
/**
|
|
* Return the NSString corresponding to the specified resource record code.
|
|
*/
|
|
static NSString*
|
|
NSStringFromRRCode(NSUInteger code)
|
|
{
|
|
/* NOTE: I still have a comprehensive plist for this in UnboundKit. I'll
|
|
* import it eventually, but for now, we need just a few rrCodes;
|
|
*/
|
|
switch (code)
|
|
{
|
|
case 0x01:
|
|
return @"A";
|
|
case 0x02:
|
|
return @"NS";
|
|
case 0x1C:
|
|
return @"AAAA";
|
|
case 0x05:
|
|
return @"CNAME";
|
|
case 0x06:
|
|
return @"SOA";
|
|
case 0x0C:
|
|
return @"PTR";
|
|
case 0x0D:
|
|
return @"HINFO";
|
|
case 0x0F:
|
|
return @"MX";
|
|
case 0x10:
|
|
return @"TXT";
|
|
case 0x21:
|
|
return @"SRV";
|
|
case 0x0A:
|
|
return @"NULL";
|
|
default:
|
|
return nil;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
|
|
/**
|
|
* Return the resource record code for the specified resource record name.
|
|
* FIXME: This is stupid and lame, I'll make it into a static NSDictionary soon:
|
|
*/
|
|
static NSUInteger
|
|
RRCodeFromNSString(NSString* name)
|
|
{
|
|
if ([name isEqualToString: @"A"])
|
|
{
|
|
return 0x01;
|
|
}
|
|
else if ([name isEqualToString: @"AAAA"])
|
|
{
|
|
return 0x1C;
|
|
}
|
|
else if ([name isEqualToString: @"TXT"])
|
|
{
|
|
return 0x10;
|
|
}
|
|
else if ([name isEqualToString: @"NULL"])
|
|
{
|
|
return 0x0A;
|
|
}
|
|
else if ([name isEqualToString: @"NS"])
|
|
{
|
|
return 0x02;
|
|
}
|
|
else if ([name isEqualToString: @"CNAME"])
|
|
{
|
|
return 0x05;
|
|
}
|
|
else if ([name isEqualToString: @"SOA"])
|
|
{
|
|
return 0x06;
|
|
}
|
|
else if ([name isEqualToString: @"PTR"])
|
|
{
|
|
return 0x0C;
|
|
}
|
|
else if ([name isEqualToString: @"HINFO"])
|
|
{
|
|
return 0x0D;
|
|
}
|
|
else if ([name isEqualToString: @"MX"])
|
|
{
|
|
return 0x0F;
|
|
}
|
|
else if ([name isEqualToString: @"SRV"])
|
|
{
|
|
return 0x21;
|
|
}
|
|
return 0x00;
|
|
}
|
|
|
|
/**
|
|
* Returns the UTF8 string or NULL, if the string is empty.
|
|
*/
|
|
static inline const char*
|
|
StringOrNullIfEmpty(NSString *domain)
|
|
{
|
|
if ((domain != nil) && ![domain isEqualToString: @""])
|
|
{
|
|
return [domain UTF8String];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Serialize an AvahiStringList into an NSData object.
|
|
*/
|
|
static NSData*
|
|
NSDataFromAvahiStringList(AvahiStringList* list)
|
|
{
|
|
/* NOTE: The record is at most 255 bytes, but should we really allocate
|
|
* 255 bytes on the stack?
|
|
*/
|
|
char *buffer = NULL;
|
|
size_t len = 0;
|
|
NSData *data = nil;
|
|
|
|
if (list == NULL)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
/* The avahi documentation on avahi_string_list_serialize() doesn't tell us
|
|
* this, but called with the arguments below, it just returns the size:
|
|
*/
|
|
len = avahi_string_list_serialize(list, NULL, 0);
|
|
if (len == 0)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
buffer = malloc(len);
|
|
// It's better to zero the buffer: memset(buffer, '\0', len);
|
|
if (buffer == NULL)
|
|
{
|
|
// Should we raise an exception?
|
|
NSDebugLog(@"Couldn't allocate %"PRIuPTR" bytes for txt record",
|
|
(uintptr_t)len);
|
|
return nil;
|
|
}
|
|
|
|
/* Proper serialization of the string list, we ignore the returned length
|
|
* since we already know it.
|
|
*/
|
|
avahi_string_list_serialize(list, buffer, len);
|
|
data = [NSData dataWithBytes: buffer
|
|
length: len];
|
|
free(buffer);
|
|
return data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Try to parse the <code>txtData</code> into an AvahiStringList. Returns 0 on
|
|
* success.
|
|
*/
|
|
static int
|
|
GSAvahiStringListFromNSData(NSData *txtData, AvahiStringList **list)
|
|
{
|
|
if (nil != txtData)
|
|
{
|
|
const void *bytes = [txtData bytes];
|
|
size_t len = [txtData length];
|
|
return avahi_string_list_parse(bytes, len, list);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Creates an <code>NSData</code> object (wrapping a <code>struct
|
|
* sockaddr_storage</code>) from the necessary Avahi data structures.
|
|
*/
|
|
static NSData*
|
|
NSDataFromAvahiAddressPortAndInterface(const AvahiAddress *addr,
|
|
uint16_t port,
|
|
AvahiIfIndex iface)
|
|
{
|
|
|
|
struct sockaddr_storage s;
|
|
size_t s_len = sizeof(struct sockaddr_storage);
|
|
if ((addr->proto != AVAHI_PROTO_INET) && (addr->proto != AVAHI_PROTO_INET6))
|
|
{
|
|
//We can't fill the struct if AVAHI_PROTO_UNSPEC
|
|
return nil;
|
|
}
|
|
// POSIX says we shall zero the structure:
|
|
memset(&s, '\0', s_len);
|
|
|
|
if (addr->proto == AVAHI_PROTO_INET)
|
|
{
|
|
struct sockaddr_in *s4 = (struct sockaddr_in*)&s;
|
|
// The address is already in network byte-order.
|
|
struct in_addr a;
|
|
a.s_addr = addr->data.ipv4.address;
|
|
s4->sin_family = AF_INET;
|
|
s4->sin_port = htons(port);
|
|
s4->sin_addr = a;
|
|
}
|
|
else if (addr->proto == AVAHI_PROTO_INET6)
|
|
{
|
|
struct sockaddr_in6 *s6 = (struct sockaddr_in6*)&s;
|
|
struct in6_addr a;
|
|
// Copy the address from the avahi structure to in6_addr:
|
|
memcpy(&(a.s6_addr), &(addr->data.ipv6.address), 16);
|
|
s6->sin6_family = AF_INET6;
|
|
s6->sin6_port = htons(port);
|
|
s6->sin6_addr = a;
|
|
if (IN6_IS_ADDR_LINKLOCAL(&a))
|
|
{
|
|
// For a link-local address set the interface index
|
|
// NOTE: These are implementation-defined but Avahi doesn't seem to
|
|
// mangle them.
|
|
s6->sin6_scope_id = iface;
|
|
}
|
|
else
|
|
{
|
|
// FIXME: Do nothing. The structure was zeroed out. Please pray
|
|
// that every sensible implementation has 0 for the global
|
|
// scope. (Most of the time, people will be announcing link-local
|
|
// addresses, though)
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Shouldn't happen:
|
|
return nil;
|
|
}
|
|
return [NSData dataWithBytes: &s
|
|
length: s_len];
|
|
}
|
|
|
|
/**
|
|
* Creates an <code>NSData</code> object wrapping a <code>struct
|
|
* sockaddr_storage</code> from an <code>NSData</code> object that contains a
|
|
* plain IPv4 or IPv6 address in network byte-order along with the necessary
|
|
* <code>port</code>, address <code>family</code> and interface
|
|
* <code>index</code> information.
|
|
*/
|
|
static NSData*
|
|
NSDataWithAddrDataPortFamilyAndIface(NSData *old,
|
|
NSUInteger port,
|
|
AvahiProtocol family,
|
|
AvahiIfIndex index)
|
|
{
|
|
AvahiAddress addr;
|
|
const void *addrBytes;
|
|
if (old == nil)
|
|
{
|
|
return nil;
|
|
}
|
|
addr.proto = family;
|
|
addrBytes = [old bytes];
|
|
//Copy the value into the data field:
|
|
if (family == AVAHI_PROTO_INET)
|
|
{
|
|
memcpy(addr.data.data, addrBytes, 4);
|
|
}
|
|
else if (family == AVAHI_PROTO_INET6)
|
|
{
|
|
memcpy(addr.data.data, addrBytes, 16);
|
|
}
|
|
else
|
|
{
|
|
return nil;
|
|
}
|
|
return NSDataFromAvahiAddressPortAndInterface(&addr, port, index);
|
|
}
|
|
|
|
/**
|
|
* This callback function will be called by the Avahi layer for resolver events.
|
|
*/
|
|
static void
|
|
GSAvahiServiceResolverEvent(
|
|
AvahiServiceResolver *resolver,
|
|
AvahiIfIndex interface,
|
|
AvahiProtocol protocol,
|
|
AvahiResolverEvent event,
|
|
const char *name,
|
|
const char *type,
|
|
const char *domain,
|
|
const char *hostName,
|
|
const AvahiAddress *addr,
|
|
uint16_t port,
|
|
AvahiStringList *txtRecord,
|
|
AvahiLookupResultFlags flags,
|
|
void *userInfo
|
|
)
|
|
{
|
|
GSAvahiNetService *service = nil;
|
|
if (NULL == resolver)
|
|
{
|
|
NSDebugLog(@"NULL pointer to AvahiServiceResolver.");
|
|
return;
|
|
}
|
|
if (NULL == userInfo)
|
|
{
|
|
NSDebugLog(@"NULL pointer to NSNetService.");
|
|
return;
|
|
}
|
|
service = (GSAvahiNetService*)userInfo;
|
|
|
|
switch (event)
|
|
{
|
|
case AVAHI_RESOLVER_FAILURE:
|
|
[service handleError:
|
|
avahi_client_errno(avahi_service_resolver_get_client(resolver))];
|
|
break;
|
|
|
|
case AVAHI_RESOLVER_FOUND:
|
|
|
|
[service avahiResolver: resolver
|
|
foundServiceWithName: NSStringIfNotNull(name)
|
|
type: NSStringIfNotNull(type)
|
|
domain: GSNetServiceDotTerminatedNSStringFromString(domain)
|
|
hostName: NSStringIfNotNull(hostName)
|
|
address: NSDataFromAvahiAddressPortAndInterface(addr, port, interface)
|
|
port: (NSInteger)port
|
|
txtRecord: NSDataFromAvahiStringList(txtRecord)
|
|
ifIndex: interface
|
|
protocol: protocol];
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This callback function will be called by the Avahi layer on events for a
|
|
* record browser.
|
|
*/
|
|
static void
|
|
GSAvahiRecordBrowserEvent(
|
|
AvahiRecordBrowser *browser,
|
|
AvahiIfIndex interface,
|
|
AvahiProtocol protocol,
|
|
AvahiBrowserEvent event,
|
|
const char *name,
|
|
uint16_t dnsClass,
|
|
uint16_t type,
|
|
const void *rdata,
|
|
size_t size,
|
|
AvahiLookupResultFlags flags,
|
|
void *userInfo)
|
|
{
|
|
GSAvahiNetService *service = nil;
|
|
NSData *recordData = nil;
|
|
if (NULL == browser)
|
|
{
|
|
NSDebugLog(@"NULL pointer to AvahiServiceResolver.");
|
|
return;
|
|
}
|
|
if (NULL == userInfo)
|
|
{
|
|
NSDebugLog(@"NULL pointer to NSNetService.");
|
|
return;
|
|
}
|
|
service = (GSAvahiNetService*)userInfo;
|
|
|
|
if (type == 0 && (event != AVAHI_BROWSER_FAILURE))
|
|
{
|
|
[service handleError: NSNetServicesInvalidError];
|
|
return;
|
|
}
|
|
|
|
if (rdata != NULL)
|
|
{
|
|
recordData = [NSData dataWithBytes: rdata
|
|
length: size];
|
|
}
|
|
switch (event)
|
|
{
|
|
case AVAHI_BROWSER_NEW:
|
|
[service newData: recordData
|
|
forRRCode: type];
|
|
break;
|
|
|
|
case AVAHI_BROWSER_REMOVE:
|
|
[service removedData: recordData
|
|
forRRCode: type];
|
|
break;
|
|
|
|
case AVAHI_BROWSER_FAILURE:
|
|
[service handleError:
|
|
avahi_client_errno(avahi_record_browser_get_client(browser))
|
|
forRRCode: type];
|
|
break;
|
|
|
|
case AVAHI_BROWSER_CACHE_EXHAUSTED: // Not interesting
|
|
break;
|
|
|
|
case AVAHI_BROWSER_ALL_FOR_NOW:
|
|
[service allForNowForRRCode: type];
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This callback function will be called by the Avahi layer to notify about
|
|
* state changes in an entry group.
|
|
*/
|
|
static void
|
|
GSAvahiEntryGroupStateChanged(AvahiEntryGroup *group,
|
|
AvahiEntryGroupState state,
|
|
void *userInfo)
|
|
{
|
|
if (NULL == group)
|
|
{
|
|
NSDebugLog(@"NULL pointer to AvahiEntryGroup.");
|
|
return;
|
|
}
|
|
if (NULL == userInfo)
|
|
{
|
|
NSDebugLog(@"NULL pointer to NSNetService.");
|
|
return;
|
|
}
|
|
[(GSAvahiNetService*)userInfo entryGroup: group
|
|
enteredState: state];
|
|
}
|
|
|
|
@implementation GSAvahiNetService
|
|
|
|
+ (void) initialize
|
|
{
|
|
if (self == [GSAvahiNetService class])
|
|
{
|
|
GSObjCAddClassBehavior(self, [GSAvahiClient class]);
|
|
}
|
|
}
|
|
|
|
+ (NSData *) dataFromTXTRecordDictionary: (NSDictionary *) txtDictionary
|
|
{
|
|
// This NULL avahi-string list is the terminating element of the linked
|
|
// list.
|
|
AvahiStringList *list = NULL;
|
|
NSArray *keys = [txtDictionary allKeys];
|
|
NSData *data = nil;
|
|
|
|
FOR_IN(NSString*, key, keys)
|
|
{
|
|
id value = [txtDictionary objectForKey: key];
|
|
if ([value isKindOfClass: [NSString class]])
|
|
{
|
|
list = avahi_string_list_add_pair(list,
|
|
[key UTF8String], [value UTF8String]);
|
|
}
|
|
else if ([value isKindOfClass: [NSNumber class]])
|
|
{
|
|
list = avahi_string_list_add_pair(list,
|
|
[key UTF8String], [[(NSNumber*)value stringValue] UTF8String]);
|
|
}
|
|
else if ([value isKindOfClass: [NSData class]])
|
|
{
|
|
NSUInteger len = [value length];
|
|
/* TXT record can only contain 256bytes, so there is no point in
|
|
* handling NSData values with len > 255.
|
|
*/
|
|
if (len <= 255)
|
|
{
|
|
list = avahi_string_list_add_pair_arbitrary(list,
|
|
[key UTF8String], [value bytes], len);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* We cannot handle any other type of value. We thus free the list
|
|
* and make sure we fail the subsequent assertion (because this is
|
|
* what the Apple documentation will do.
|
|
*/
|
|
if (list)
|
|
{
|
|
avahi_string_list_free(list);
|
|
list = NULL;
|
|
}
|
|
}
|
|
NSAssert(list,@"Error creating string list for TXT record");
|
|
}
|
|
END_FOR_IN(keys)
|
|
|
|
// Convert string list into a data object:
|
|
data = NSDataFromAvahiStringList(list);
|
|
avahi_string_list_free(list);
|
|
list = NULL;
|
|
return data;
|
|
}
|
|
|
|
+ (NSDictionary *) dictionaryFromTXTRecordData: (NSData *) txtData
|
|
{
|
|
AvahiStringList *list = NULL;
|
|
int ret = 0;
|
|
NSMutableDictionary *dict = nil;
|
|
AvahiStringList *item = NULL;
|
|
NSAssert(([txtData length] > 0),
|
|
@"No data to convert to TXT record dictionary");
|
|
ret = GSAvahiStringListFromNSData(txtData, &list);
|
|
// ret == 0 on success.
|
|
NSAssert(!ret, @"Could not parse TXT data");
|
|
if (list == NULL)
|
|
{
|
|
// This means the txtData was empty (e.g. only zeros)
|
|
return nil;
|
|
}
|
|
// Autoreleased:
|
|
dict = [NSMutableDictionary dictionary];
|
|
|
|
// Use the beginning of the list as the first element to handle:
|
|
item = list;
|
|
do
|
|
{
|
|
char *key = NULL;
|
|
char *value = NULL;
|
|
size_t len = 0;
|
|
NSString *k = nil;
|
|
NSData *v = nil;
|
|
int ret = avahi_string_list_get_pair(item, &key, &value, &len);
|
|
// ret == 0 indicates success.
|
|
NSAssert(!ret, @"Could not create TXT record dictionary.");
|
|
if (key)
|
|
{
|
|
k = [NSString stringWithUTF8String: key];
|
|
avahi_free(key);
|
|
}
|
|
|
|
if (value)
|
|
{
|
|
v = [NSData dataWithBytes: (void*)value
|
|
length: len];
|
|
avahi_free(value);
|
|
}
|
|
else
|
|
{
|
|
// If the value is empty, place an empty NSData object in the
|
|
// dictionary.
|
|
v = [NSData data];
|
|
}
|
|
NSAssert((k && v),@"Could not create TXT record dictionary");
|
|
[dict setObject: v forKey: k];
|
|
}
|
|
while (NULL != (item = avahi_string_list_get_next(item)));
|
|
avahi_string_list_free(list);
|
|
return dict;
|
|
}
|
|
|
|
|
|
|
|
- (void) netService: (NSNetService*)service
|
|
didUpdateAddresses: (NSArray*)addresses
|
|
{
|
|
if ([[self delegate] respondsToSelector:
|
|
@selector(netService:didUpdateAddresses:)])
|
|
{
|
|
[(id)[self delegate] netService: service
|
|
didUpdateAddresses: addresses];
|
|
}
|
|
}
|
|
|
|
- (void) netService: (NSNetService*)service
|
|
didUpdateRecordData: (id)data
|
|
forRecordType: (NSString*)rrType
|
|
{
|
|
SEL theSelector = NULL;
|
|
if ([rrType isEqualToString: @"A"] || [rrType isEqualToString: @"AAAA"])
|
|
{
|
|
[self netService: service didUpdateAddresses: [self addresses]];
|
|
}
|
|
theSelector = NSSelectorFromString([NSString stringWithFormat:
|
|
@"netService:didUpdate%@RecordData:", rrType]);
|
|
if ([[self delegate] respondsToSelector: theSelector])
|
|
{
|
|
if (([rrType isEqualToString: @"TXT"])
|
|
&& [data isKindOfClass: [NSArray class]])
|
|
{
|
|
/*
|
|
* Legacy case for TXT records (user code will always expect NSData,
|
|
* not NSArray).
|
|
*/
|
|
data = [(NSArray*)data lastObject];
|
|
}
|
|
[[self delegate] performSelector: theSelector
|
|
withObject: service
|
|
withObject: data];
|
|
}
|
|
else if ([[self delegate] respondsToSelector:
|
|
@selector(netService:didUpdateRecordData:forRecordType:)])
|
|
{
|
|
[(id)[self delegate] netService: service
|
|
didUpdateRecordData: data
|
|
forRecordType: rrType];
|
|
}
|
|
}
|
|
|
|
- (void) netService: (NSNetService*)service
|
|
didNotMonitor: (NSDictionary*)errorDict
|
|
forRecordType: (NSString*)rrType
|
|
{
|
|
SEL theSelector = NSSelectorFromString([NSString stringWithFormat:
|
|
@"netService:didNotMonitor%@RecordData:", rrType]);
|
|
|
|
if ([[self delegate] respondsToSelector: theSelector])
|
|
{
|
|
[[self delegate] performSelector: theSelector
|
|
withObject: service
|
|
withObject: errorDict];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Designated intializer that passes interface index and protocol information
|
|
* alongside the usual information for a mDNS service. This is used by
|
|
* GSAvahiNetServiceBrowser which already knows about these.
|
|
*/
|
|
- (id) initWithDomain: (NSString*)domain
|
|
type: (NSString*)type
|
|
name: (NSString*)name
|
|
port: (NSInteger)port
|
|
avahiIfIndex: (AvahiIfIndex)anIfIndex
|
|
avahiProtocol: (AvahiProtocol)aProtocol
|
|
{
|
|
const NSMapTableValueCallBacks valueCallbacks = {NULL, NULL, NULL};
|
|
if (nil == (self = [self avahiClientInit]))
|
|
{
|
|
return nil;
|
|
}
|
|
_infoLock = [[NSRecursiveLock alloc] init];
|
|
_info = [[NSMutableDictionary alloc] init];
|
|
[_info setObject: domain forKey: @"domain"];
|
|
[_info setObject: type forKey: @"type"];
|
|
[_info setObject: name forKey: @"name"];
|
|
[_info setObject: [NSNumber numberWithInteger: port]
|
|
forKey: @"port"];
|
|
_browsers = NSCreateMapTable(NSIntegerMapKeyCallBacks, valueCallbacks, 10);
|
|
_browserTimeouts = NSCreateMapTable(NSIntegerMapKeyCallBacks,
|
|
NSObjectMapValueCallBacks, 10);
|
|
if (port > 0)
|
|
{
|
|
// If port is set to a sensible value in this initializer, we are
|
|
// initialized to publish and will create the entry group.
|
|
}
|
|
_ifIndex = anIfIndex;
|
|
_protocol = aProtocol;
|
|
return self;
|
|
}
|
|
|
|
- (id) initWithDomain: (NSString *) domain
|
|
type: (NSString *) type
|
|
name: (NSString *) name
|
|
avahiIfIndex: (AvahiIfIndex)index
|
|
avahiProtocol: (AvahiProtocol)proto
|
|
{
|
|
return [self initWithDomain: domain
|
|
type: type
|
|
name: name
|
|
port: -1
|
|
avahiIfIndex: index
|
|
avahiProtocol: proto];
|
|
}
|
|
|
|
- (id) initWithDomain: (NSString *) domain
|
|
type: (NSString *) type
|
|
name: (NSString *) name
|
|
{
|
|
return [self initWithDomain: domain
|
|
type: type
|
|
name: name
|
|
port: -1];
|
|
}
|
|
- (id) initWithDomain: (NSString *) domain
|
|
type: (NSString *) type
|
|
name: (NSString *) name
|
|
port: (NSInteger) port
|
|
{
|
|
return [self initWithDomain: domain
|
|
type: type
|
|
name: name
|
|
port: port
|
|
avahiIfIndex: AVAHI_IF_UNSPEC
|
|
avahiProtocol: AVAHI_PROTO_UNSPEC];
|
|
}
|
|
|
|
/**
|
|
* Stores an object at a key in the info dictionary in a synchronized fashion.
|
|
* This uses a SeqLock pattern: It obtains the lock associated with the
|
|
* dictionary, increments a sequence number, performs the operation, increases
|
|
* the sequence number again, and releases the lock. This way it is garantueed
|
|
* that the sequence number is odd while an operation is in progress, and even
|
|
* if it is safe for a reader to obtain the value.
|
|
*/
|
|
- (void)setInfoObject: (id)object
|
|
forKey: (NSString*)key
|
|
{
|
|
if ((object == nil) || (key == nil))
|
|
{
|
|
return;
|
|
}
|
|
[_infoLock lock];
|
|
_infoSeq++;
|
|
// Now the sequence number is odd: Write in progess.
|
|
[_info setObject: object
|
|
forKey: key];
|
|
_infoSeq++;
|
|
// Now the sequence number is even: Can be read
|
|
[_infoLock unlock];
|
|
}
|
|
|
|
/**
|
|
* Removes an object from the dictionary in a synchronized fashion. Cf. note
|
|
* about -setInfoObject:forKey:.
|
|
*/
|
|
- (void) removeInfoObjectForKey: (NSString*)key
|
|
{
|
|
if (key == nil)
|
|
{
|
|
return;
|
|
}
|
|
[_infoLock lock];
|
|
_infoSeq++;
|
|
// Now the sequence number is odd: Write in progess.
|
|
[_info removeObjectForKey: key];
|
|
_infoSeq++;
|
|
// Now the sequence number is even: Can be read
|
|
[_infoLock unlock];
|
|
}
|
|
|
|
/**
|
|
* Thread-safe (and fast) reading of an info key by comparing the sequence
|
|
* numbers set by the writers. Cf. note about -setInfoObject:forKey:.
|
|
*/
|
|
- (id) infoObjectForKey: (NSString*)key
|
|
{
|
|
NSUInteger oldSeq = 0;
|
|
NSUInteger newSeq = 0;
|
|
BOOL isOdd = NO;
|
|
id object = nil;
|
|
if (key == nil)
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
// Try to read the value until the sequence numbers match and are even:
|
|
do
|
|
{
|
|
// Store the sequence number before the read:
|
|
oldSeq = _infoSeq;
|
|
// Read the value:
|
|
object = [_info objectForKey: key];
|
|
// Store the sequence number after the read:
|
|
newSeq = _infoSeq;
|
|
|
|
while ((newSeq == 0) || (oldSeq == 0))
|
|
{
|
|
//Add 2 to prevent (0 % 2) because it's undefined.
|
|
newSeq = newSeq + 2;
|
|
oldSeq = oldSeq +2;
|
|
}
|
|
isOdd = (BOOL)newSeq % 2;
|
|
} while ((oldSeq != newSeq) || (isOdd));
|
|
|
|
return object;
|
|
}
|
|
|
|
- (NSInteger) port
|
|
{
|
|
NSInteger port = [(NSNumber*)[self infoObjectForKey: @"port"] integerValue];
|
|
return port ? MAX(port, -1) : -1;
|
|
}
|
|
|
|
|
|
/**
|
|
* Private method that creates an empty entry group that needs to be filled
|
|
* before publication. There is no locking here because it will only be called
|
|
* from methods that already obtained the lock. Returns 0 on success.
|
|
*/
|
|
- (int) createEntryGroup
|
|
{
|
|
// We only care to publish if the service is still idle:
|
|
if (_serviceState != GSNetServiceIdle)
|
|
{
|
|
[self netService: self
|
|
didNotPublish: [self errorDictWithErrorCode: NSNetServicesActivityInProgress]];
|
|
return NSNetServicesActivityInProgress;
|
|
}
|
|
|
|
//Create the entry group:
|
|
if (NULL != _client)
|
|
{
|
|
_entryGroup = avahi_entry_group_new((AvahiClient*)_client,
|
|
GSAvahiEntryGroupStateChanged,
|
|
(void*)self);
|
|
}
|
|
else
|
|
{
|
|
// having no _client usually means that avahi-daemon (or dbus)
|
|
// isn't running, unfortunately there's no precise errNo at this point
|
|
// so we're providing just our best guess
|
|
return AVAHI_ERR_NO_DAEMON;
|
|
}
|
|
|
|
// Handle error:
|
|
if (NULL == _entryGroup)
|
|
{
|
|
int error = avahi_client_errno((AvahiClient*)_client);
|
|
[self handleError: error];
|
|
return error;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Private method to commit an entry group to the network. The entry group
|
|
* cannot be modified afterwards. There is no locking here because it will only
|
|
* be called from methods that already obtained the lock. Notifies the delegate
|
|
* on error or success.
|
|
*/
|
|
- (void)commitEntryGroup
|
|
{
|
|
_serviceState = GSNetServicePublishing;
|
|
// Make sure there is an entry group to commit:
|
|
if (_entryGroup != NULL)
|
|
{
|
|
// Make sure it is not empty:
|
|
if (!avahi_entry_group_is_empty((AvahiEntryGroup*)_entryGroup))
|
|
{
|
|
// Make sure it is not already committed:
|
|
if (avahi_entry_group_get_state((AvahiEntryGroup*)_entryGroup)
|
|
== AVAHI_ENTRY_GROUP_UNCOMMITED)
|
|
{
|
|
avahi_entry_group_commit((AvahiEntryGroup*)_entryGroup);
|
|
[self netServiceWillPublish: self];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// The entryGroup is active:
|
|
[self handleError: NSNetServicesActivityInProgress];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// The entryGroup is not properly set up for publication:
|
|
[self handleError: NSNetServicesBadArgumentError];
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Private method to add a service entry to the entry group. Will only be called
|
|
* from methods that already ensure that the entry can be added safely.
|
|
*/
|
|
- (int) addServiceEntry
|
|
{
|
|
int ret = 0;
|
|
const char* d = StringOrNullIfEmpty([self infoObjectForKey: @"domain"]);
|
|
|
|
// It is possible that the TXT record has already been set, so we can
|
|
// publish it right away:
|
|
AvahiStringList *list = NULL;
|
|
NSData *txtData = [self infoObjectForKey: @"TXT"];
|
|
int res = 0;
|
|
res = GSAvahiStringListFromNSData(txtData, &list);
|
|
if (0 != res)
|
|
{
|
|
if (NULL != list)
|
|
{
|
|
avahi_string_list_free(list);
|
|
list = NULL;
|
|
}
|
|
}
|
|
ret = avahi_entry_group_add_service_strlst((AvahiEntryGroup*)_entryGroup,
|
|
_ifIndex,
|
|
_protocol,
|
|
0, // Flags, we don't need them
|
|
[(NSString*)[self infoObjectForKey: @"name"] UTF8String],
|
|
[(NSString*)[self infoObjectForKey: @"type"] UTF8String],
|
|
d, // Domain (might be NULL for default)
|
|
NULL, // The hostname is filled automatically
|
|
[[self infoObjectForKey: @"port"] integerValue],
|
|
list // Possibly empty TXT record
|
|
);
|
|
if (NULL != list)
|
|
{
|
|
// If we are not using the emptyList from the stack, we need to free the
|
|
// list that avahi created for us.
|
|
avahi_string_list_free(list);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
- (BOOL) addServiceRecordWithOptions: (NSNetServiceOptions)options
|
|
{
|
|
int ret = 0;
|
|
[_lock lock];
|
|
if (_serviceState != GSNetServiceIdle)
|
|
{
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
|
|
if (_entryGroup == NULL)
|
|
{
|
|
if (0 != [self createEntryGroup])
|
|
{
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (options & NSNetServiceListenForConnections)
|
|
{
|
|
GSServerStream *serverStream;
|
|
NSInteger port;
|
|
|
|
/* setup server socket first, as port is required in
|
|
* -[self addServiceEntry] (see below)
|
|
*/
|
|
port = [self port];
|
|
if (port < 0)
|
|
{
|
|
port = 0;
|
|
}
|
|
serverStream = [GSServerStream serverStreamToAddr: @"" port: port];
|
|
if (serverStream != nil)
|
|
{
|
|
[serverStream setDelegate:self];
|
|
[serverStream open];
|
|
if ([serverStream streamStatus] != NSStreamStatusOpen)
|
|
{
|
|
ret = 1;
|
|
}
|
|
else
|
|
{
|
|
NSNumber *portNumber;
|
|
|
|
[serverStream scheduleInRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[self setInfoObject: serverStream forKey: @"serverStream"];
|
|
portNumber = [serverStream propertyForKey: GSStreamLocalPortKey];
|
|
[self setInfoObject: portNumber forKey: @"port"];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ret = 1;
|
|
}
|
|
|
|
if (ret != 0)
|
|
{
|
|
[self handleError: NSNetServicesBadArgumentError];
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
/* Try adding the service to the entry group until we find an unused name
|
|
* for it (but only if NSNetServiceNoAutoRename is not set).
|
|
*/
|
|
while (AVAHI_ERR_COLLISION == (ret = [self addServiceEntry])
|
|
&& !(options & NSNetServiceNoAutoRename))
|
|
{
|
|
char *newName = avahi_alternative_service_name([[self infoObjectForKey:
|
|
@"name"] UTF8String]);
|
|
if (newName)
|
|
{
|
|
[self setInfoObject: [NSString stringWithUTF8String: newName]
|
|
forKey: @"name"];
|
|
avahi_free(newName);
|
|
}
|
|
else
|
|
{
|
|
ret = AVAHI_ERR_FAILURE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
[_lock unlock];
|
|
return ret == 0 ? YES : NO;
|
|
}
|
|
|
|
- (BOOL) addServiceRecord
|
|
{
|
|
return [self addServiceRecordWithOptions: 0];
|
|
}
|
|
|
|
- (void) publishWithOptions: (NSNetServiceOptions)options
|
|
{
|
|
[_lock lock];
|
|
if (_entryGroup == NULL)
|
|
{
|
|
if (NO == [self addServiceRecordWithOptions: options])
|
|
{
|
|
[self handleError: _client ? avahi_client_errno((AvahiClient*)_client)
|
|
: AVAHI_ERR_NO_DAEMON];
|
|
}
|
|
}
|
|
[self commitEntryGroup];
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Convenience method to return the full name of the service.
|
|
*/
|
|
- (NSString *) fullServiceName
|
|
{
|
|
NSString *full = nil;
|
|
NSString *domain = [self infoObjectForKey: @"domain"];
|
|
|
|
if (([domain isEqualToString: @""]) || domain == nil)
|
|
{
|
|
// Pick the default domain:
|
|
domain = [NSString stringWithUTF8String:
|
|
avahi_client_get_domain_name((AvahiClient*)_client)];
|
|
}
|
|
full = [NSString stringWithFormat: @"%@.%@.%@", [self name],
|
|
[self type], domain];
|
|
if ((unichar)'.' != [full characterAtIndex: ([full length] - 1)])
|
|
{
|
|
return [full stringByAppendingString: @"."];
|
|
}
|
|
return full;
|
|
}
|
|
|
|
- (void) publish
|
|
{
|
|
// Publish and allow renaming:
|
|
[self publishWithOptions: 0];
|
|
}
|
|
|
|
- (void) resolve
|
|
{
|
|
[self resolveWithTimeout: 5];
|
|
}
|
|
|
|
/**
|
|
* Called by the resolver timeout to cease service resolution.
|
|
*/
|
|
- (void) didTimeout: (NSTimer*)theTimer
|
|
{
|
|
[self stop];
|
|
_timer = nil;
|
|
}
|
|
|
|
- (void) resolveWithTimeout: (NSTimeInterval) timeout
|
|
{
|
|
if (_serviceState < GSNetServiceResolving)
|
|
{
|
|
_resolver = (void*)avahi_service_resolver_new((AvahiClient*)_client,
|
|
_ifIndex,
|
|
_protocol,
|
|
[[self infoObjectForKey: @"name"] UTF8String],
|
|
[[self infoObjectForKey: @"type"] UTF8String],
|
|
[[self infoObjectForKey: @"domain"] UTF8String],
|
|
AVAHI_PROTO_UNSPEC,
|
|
0,
|
|
GSAvahiServiceResolverEvent,
|
|
(void*)self);
|
|
|
|
if (NULL == _resolver)
|
|
{
|
|
[self handleError: avahi_client_errno((AvahiClient*)_client)];
|
|
return;
|
|
}
|
|
_serviceState = GSNetServiceResolving;
|
|
if (_timer != nil)
|
|
{
|
|
[_timer invalidate];
|
|
_timer = nil;
|
|
}
|
|
_timer = [[NSTimer timerWithTimeInterval: timeout
|
|
target: self
|
|
selector: @selector(didTimeout:)
|
|
userInfo: nil
|
|
repeats: NO] retain];
|
|
|
|
[[ctx runLoop] addTimer: _timer
|
|
forMode: [ctx mode]];
|
|
}
|
|
// BOOM
|
|
}
|
|
|
|
/**
|
|
* Main cleanup method. Will clean up all avahi-related resources in use by
|
|
* the GSAvahiNetService. An resource record code of -1 indicates that that
|
|
* the whole service needs to be reset. Otherwise, it will only clean up the
|
|
* browser for the specified record type.
|
|
*/
|
|
- (void) stopWithError: (BOOL)hadError
|
|
forRRCode: (NSInteger)rrCode
|
|
{
|
|
/*
|
|
* If an RRCode was set (a value of zero possibly indicating an unknown
|
|
* RRCode, fo which we won't do anything), we only clean up the
|
|
* corresponding browser.
|
|
*/
|
|
if (rrCode == 0)
|
|
{
|
|
return;
|
|
}
|
|
[_lock lock];
|
|
if (rrCode != -1)
|
|
{
|
|
AvahiRecordBrowser *browser
|
|
= NSMapGet(_browsers, (void*)(uintptr_t)rrCode);
|
|
if (browser != NULL)
|
|
{
|
|
avahi_record_browser_free(browser);
|
|
NSMapRemove(_browsers, (void*)(uintptr_t)rrCode);
|
|
[(NSTimer*)NSMapGet(_browserTimeouts,
|
|
(void*)(uintptr_t)rrCode) invalidate];
|
|
NSMapRemove(_browserTimeouts, (void*)(uintptr_t)rrCode);
|
|
if ((NSCountMapTable(_browsers) == 0)
|
|
&& (_serviceState == GSNetServiceRecordBrowsing))
|
|
{
|
|
_serviceState = GSNetServiceResolved;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (_timer != nil)
|
|
{
|
|
[_timer invalidate];
|
|
_timer = nil;
|
|
}
|
|
if (_resolver != NULL)
|
|
{
|
|
avahi_service_resolver_free((AvahiServiceResolver*)_resolver);
|
|
_resolver = NULL;
|
|
}
|
|
if (_entryGroup != NULL)
|
|
{
|
|
//Make sure to unpublish it.
|
|
avahi_entry_group_reset((AvahiEntryGroup*)_entryGroup);
|
|
avahi_entry_group_free((AvahiEntryGroup*)_entryGroup);
|
|
_entryGroup = NULL;
|
|
}
|
|
|
|
if ((_browsers != NULL) && (0 != NSCountMapTable(_browsers)))
|
|
{
|
|
NSMapTable *enumerationTable;
|
|
NSMapEnumerator bEnum;
|
|
NSUInteger code;
|
|
AvahiRecordBrowser *browser;
|
|
|
|
enumerationTable
|
|
= NSCopyMapTableWithZone(_browsers, NSDefaultMallocZone());
|
|
bEnum = NSEnumerateMapTable(enumerationTable);
|
|
while (NSNextMapEnumeratorPair(&bEnum,(void*)&code,(void*)&browser))
|
|
{
|
|
avahi_record_browser_free(browser);
|
|
NSMapRemove(_browsers, (void*)code);
|
|
[(NSTimer*)NSMapGet(_browserTimeouts,
|
|
(void*)(uintptr_t)code) invalidate];
|
|
NSMapRemove(_browserTimeouts, (void*)(uintptr_t)code);
|
|
}
|
|
NSEndMapTableEnumeration(&bEnum);
|
|
NSFreeMapTable(enumerationTable);
|
|
}
|
|
if (!hadError)
|
|
{
|
|
[self removeInfoObjectForKey: @"serverStream"];
|
|
[self netServiceDidStop: self];
|
|
}
|
|
_serviceState = GSNetServiceIdle;
|
|
[_lock unlock];
|
|
}
|
|
|
|
- (void) stop
|
|
{
|
|
[self stopWithError: NO
|
|
forRRCode: -1];
|
|
}
|
|
|
|
- (void) startMonitoringForRecordType: (NSString*)rrType
|
|
{
|
|
NSUInteger code = RRCodeFromNSString(rrType);
|
|
AvahiRecordBrowser *browser = NULL;
|
|
// Raise error for bad record type:
|
|
if (code == 0)
|
|
{
|
|
[self handleError: NSNetServicesBadArgumentError
|
|
forRRCode: code];
|
|
return;
|
|
}
|
|
[_lock lock];
|
|
if ((_serviceState < GSNetServiceResolved)
|
|
|| (_serviceState > GSNetServiceRecordBrowsing))
|
|
{
|
|
[self handleError: NSNetServicesBadArgumentError
|
|
forRRCode: code];
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
|
|
browser = NSMapGet(_browsers, (void*)(uintptr_t)code);
|
|
if (browser)
|
|
{
|
|
NSDebugLog(@"Browser for RR code %@ already monitoring", rrType);
|
|
[self handleError: NSNetServicesActivityInProgress];
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
browser = avahi_record_browser_new((AvahiClient*)_client,
|
|
_ifIndex,
|
|
_protocol,
|
|
[[self fullServiceName] UTF8String],
|
|
AVAHI_DNS_CLASS_IN,
|
|
(uint16_t)code,
|
|
0,
|
|
GSAvahiRecordBrowserEvent,
|
|
self);
|
|
if (browser == NULL)
|
|
{
|
|
// Something went wrong:
|
|
[self handleError: avahi_client_errno((AvahiClient*)_client)];
|
|
}
|
|
else
|
|
{
|
|
// The browser was successfully created, we add it to the mapTable.
|
|
NSMapInsert(_browsers, (void*)(uintptr_t)code, browser);
|
|
// Set the proper state if the new browser is responsible for a state
|
|
// change.
|
|
if (_serviceState == GSNetServiceResolved)
|
|
{
|
|
_serviceState = GSNetServiceRecordBrowsing;
|
|
}
|
|
}
|
|
[_lock unlock];
|
|
}
|
|
|
|
- (void) stopMonitoringForRecordType: (NSString*)rrType
|
|
{
|
|
NSUInteger rrCode = RRCodeFromNSString(rrType);
|
|
AvahiRecordBrowser *browser = NULL;
|
|
[_lock lock];
|
|
if (_serviceState > GSNetServiceRecordBrowsing)
|
|
{
|
|
//Don't do anything for a publishing service.
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
browser = NSMapGet(_browsers, (void*)(uintptr_t)rrCode);
|
|
if (browser != NULL)
|
|
{
|
|
avahi_record_browser_free(browser);
|
|
}
|
|
if (0 == NSCountMapTable(_browsers))
|
|
{
|
|
_serviceState = GSNetServiceResolved;
|
|
}
|
|
[_lock unlock];
|
|
}
|
|
|
|
- (void) startMonitoring
|
|
{
|
|
if ((_serviceState == GSNetServiceResolved)
|
|
|| (_serviceState == GSNetServiceRecordBrowsing))
|
|
{
|
|
[self startMonitoringForRecordType: @"TXT"];
|
|
}
|
|
}
|
|
- (void) stopMonitoring
|
|
{
|
|
[self stopMonitoringForRecordType: @"TXT"];
|
|
}
|
|
|
|
|
|
- (NSArray *) addresses
|
|
{
|
|
NSArray *addresses = [self infoObjectForKey: @"addresses"];
|
|
if (nil == addresses)
|
|
{
|
|
// As per Apple documentation "If no addresses were resolved for the
|
|
// service, the returned array contains zero elements."
|
|
return [[[NSArray alloc] init] autorelease];
|
|
}
|
|
return addresses;
|
|
}
|
|
|
|
/**
|
|
* Private method to add new address data to the service.
|
|
*/
|
|
- (void) addAddressData: (NSData*)data
|
|
{
|
|
NSMutableArray *addresses = nil;
|
|
|
|
if (data == nil)
|
|
{
|
|
return;
|
|
}
|
|
addresses = [self infoObjectForKey: @"addresses"];
|
|
if (addresses == nil)
|
|
{
|
|
// Autoreleased:
|
|
addresses = [NSMutableArray array];
|
|
[self setInfoObject: addresses
|
|
forKey: @"addresses"];
|
|
}
|
|
if (![addresses containsObject: data])
|
|
{
|
|
[addresses addObject: data];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private method to remove stale address data from the service.
|
|
*/
|
|
- (void) removeAddressData: (NSData*)data
|
|
{
|
|
NSMutableArray *addresses = nil;
|
|
// Index of the address in the array:
|
|
NSUInteger index = NSNotFound;
|
|
if (data == nil)
|
|
{
|
|
return;
|
|
}
|
|
addresses = [self infoObjectForKey: @"addresses"];
|
|
if (addresses == nil)
|
|
{
|
|
// Autoreleased:
|
|
addresses = [NSMutableArray array];
|
|
[self setInfoObject: addresses
|
|
forKey: @"addresses"];
|
|
}
|
|
index = [addresses indexOfObjectIdenticalTo: data];
|
|
if (index != NSNotFound)
|
|
{
|
|
[addresses removeObjectAtIndex: index];
|
|
}
|
|
}
|
|
|
|
- (NSString *) domain
|
|
{
|
|
return [self infoObjectForKey: @"domain"];
|
|
}
|
|
|
|
- (NSString *) hostName
|
|
{
|
|
return [self infoObjectForKey: @"hostName"];
|
|
}
|
|
|
|
- (NSString *) name
|
|
{
|
|
return [self infoObjectForKey: @"name"];
|
|
}
|
|
|
|
- (NSString *) type
|
|
{
|
|
return [self infoObjectForKey: @"type"];
|
|
}
|
|
|
|
/**
|
|
* This method is called from the Avahi callback when a service has successfully
|
|
* resolved.
|
|
*/
|
|
- (void) avahiResolver: (AvahiServiceResolver*)aResolver
|
|
foundServiceWithName: (NSString*)name
|
|
type: (NSString*)type
|
|
domain: (NSString*)domain
|
|
hostName: (NSString*)hostName
|
|
address: (NSData*)address
|
|
port: (NSInteger)port
|
|
txtRecord: (NSData*)txtRecord
|
|
ifIndex: (AvahiIfIndex)anIfIndex
|
|
protocol: (AvahiProtocol)aProtocol
|
|
{
|
|
[_lock lock];
|
|
if ((void*)aResolver != _resolver)
|
|
{
|
|
// This callback comes from the wrong resolver:
|
|
[self handleError: NSNetServicesInvalidError];
|
|
// Free the erratic resolver, the real one will have been freed by the
|
|
// error handler.
|
|
if (NULL != aResolver)
|
|
{
|
|
avahi_service_resolver_free(aResolver);
|
|
}
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
if (![name isEqualToString: [self name]]
|
|
|| ![type isEqualToString: [self type]]
|
|
|| ![domain isEqualToString: [self domain]])
|
|
{
|
|
// This resolver callback is for the wrong service!
|
|
[self handleError: NSNetServicesInvalidError];
|
|
[_lock unlock];
|
|
return;
|
|
}
|
|
if (hostName)
|
|
{
|
|
[self setInfoObject: hostName forKey: @"hostName"];
|
|
}
|
|
if (port)
|
|
{
|
|
[self setInfoObject: [NSNumber numberWithInteger: port]
|
|
forKey: @"port"];
|
|
}
|
|
if (txtRecord)
|
|
{
|
|
[self setInfoObject: txtRecord forKey: @"TXT"];
|
|
}
|
|
|
|
if (address)
|
|
{
|
|
[self addAddressData: address];
|
|
}
|
|
|
|
// This makes sure all further actions happen on the same if/protocol
|
|
// combination (they might have been AVAHI_(IF|PROTO)_UNSPEC before).
|
|
_ifIndex = anIfIndex;
|
|
_protocol = aProtocol;
|
|
|
|
// Clean up the resolver, we don't need it anymore:
|
|
avahi_service_resolver_free((AvahiServiceResolver*)_resolver);
|
|
_resolver = NULL;
|
|
|
|
_serviceState = GSNetServiceResolved;
|
|
[self netServiceDidResolveAddress: self];
|
|
if (_timer != nil)
|
|
{
|
|
[_timer invalidate];
|
|
_timer = nil;
|
|
}
|
|
[_lock unlock];
|
|
|
|
}
|
|
|
|
/**
|
|
* Callback for timers on record browsing, to mimic AllForNow.
|
|
*/
|
|
- (void) didTimeoutRRBrowsing: (NSTimer*)aTimer
|
|
{
|
|
// Invoke our AllForNow callback, which will take care to invalidate the
|
|
// timer.
|
|
[self allForNowForRRCode:
|
|
[(NSNumber*)[aTimer userInfo] unsignedIntegerValue]];
|
|
}
|
|
|
|
/**
|
|
* Called whenever a new event appears for the resource record
|
|
* <code>code</code>. Postpones the timeout.
|
|
*/
|
|
- (void) rescheduleBrowserTimeoutForRRCode: (NSUInteger)code
|
|
{
|
|
NSTimer *aTimer = nil;
|
|
[_lock lock];
|
|
/* Do AllForNow handling to supplement what Avahi is doing. (The AllForNow
|
|
* event seems to be sent exactly once over the lifetime of the record
|
|
* browser, which makes it quite useless.
|
|
*/
|
|
aTimer = (NSTimer*)NSMapGet(_browserTimeouts, (void*)(uintptr_t)code);
|
|
if (aTimer != nil)
|
|
{
|
|
[aTimer invalidate];
|
|
NSMapRemove(_browserTimeouts, (void*)(uintptr_t)code);
|
|
}
|
|
aTimer = [NSTimer
|
|
timerWithTimeInterval: BROWSER_UPDATE_INTERVAL
|
|
target: self
|
|
selector: @selector(didTimeoutRRBrowsing:)
|
|
userInfo: [NSNumber numberWithUnsignedInteger: code]
|
|
repeats: NO];
|
|
[[ctx runLoop] addTimer: aTimer
|
|
forMode: [ctx mode]];
|
|
NSMapInsert(_browserTimeouts, (void*)(uintptr_t)code, aTimer);
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Private method to add new data for a record type.
|
|
*/
|
|
- (void) newData: (NSData*)data
|
|
forRRCode: (NSUInteger)code
|
|
{
|
|
NSString *rrType = NSStringFromRRCode(code);
|
|
id oldValue = nil;
|
|
|
|
[self rescheduleBrowserTimeoutForRRCode: code];
|
|
if (data == nil)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We dynamically transform between an NSData object and an array as the
|
|
// value for the rrType:
|
|
[_infoLock lock];
|
|
oldValue = [self infoObjectForKey: rrType];
|
|
if (oldValue == nil)
|
|
{
|
|
[self setInfoObject: data
|
|
forKey: rrType];
|
|
}
|
|
else if ([oldValue isKindOfClass: [NSData class]])
|
|
{
|
|
NSMutableArray *container = [NSMutableArray array];
|
|
[container addObject: oldValue];
|
|
[container addObject: data];
|
|
[self setInfoObject: container
|
|
forKey: rrType];
|
|
}
|
|
else if ([oldValue isKindOfClass: [NSMutableArray class]])
|
|
{
|
|
[oldValue addObject: data];
|
|
}
|
|
else
|
|
{
|
|
[_infoLock unlock];
|
|
[NSException raise: @"NSInternalInconsistencyException"
|
|
format: @"Invalid value of NSNetService info key %@", rrType];
|
|
return;
|
|
}
|
|
|
|
// Special case for addresses, they need to update the addresses key
|
|
// properly:
|
|
if ((code == AVAHI_DNS_TYPE_A) || code == AVAHI_DNS_TYPE_AAAA)
|
|
{
|
|
AvahiProtocol proto = AVAHI_PROTO_UNSPEC;
|
|
if (code == AVAHI_DNS_TYPE_A)
|
|
{
|
|
proto = AVAHI_PROTO_INET;
|
|
}
|
|
else if (code == AVAHI_DNS_TYPE_AAAA)
|
|
{
|
|
proto = AVAHI_PROTO_INET6;
|
|
}
|
|
[self addAddressData: NSDataWithAddrDataPortFamilyAndIface(data,
|
|
[self port], proto, _ifIndex)];
|
|
}
|
|
[_infoLock unlock];
|
|
}
|
|
|
|
/**
|
|
* Private method to remove stale data for a resource record.
|
|
*/
|
|
- (void) removedData: (NSData*)data
|
|
forRRCode: (NSUInteger)code
|
|
{
|
|
NSString *rrType = NSStringFromRRCode(code);
|
|
id oldValue = nil;
|
|
[self rescheduleBrowserTimeoutForRRCode: code];
|
|
if (data == nil)
|
|
{
|
|
return;
|
|
}
|
|
[_infoLock lock];
|
|
oldValue = [self infoObjectForKey: rrType];
|
|
if (oldValue == nil)
|
|
{
|
|
//Nothing to be done.
|
|
[_infoLock unlock];
|
|
return;
|
|
}
|
|
else if ([oldValue isKindOfClass: [NSData class]])
|
|
{
|
|
if ([oldValue isEqual: data])
|
|
{
|
|
[self removeInfoObjectForKey: rrType];
|
|
}
|
|
}
|
|
else if ([oldValue isKindOfClass: [NSMutableArray class]])
|
|
{
|
|
NSUInteger index = [oldValue indexOfObjectIdenticalTo: data];
|
|
if (index != NSNotFound)
|
|
{
|
|
NSUInteger count;
|
|
[oldValue removeObjectAtIndex: index];
|
|
count = [oldValue count];
|
|
if (count == 1)
|
|
{
|
|
//Go back to a plain data record:
|
|
[self setInfoObject: [oldValue objectAtIndex: 0]
|
|
forKey: rrType];
|
|
}
|
|
else if (count == 0)
|
|
{
|
|
// Remove empty array:
|
|
[self removeInfoObjectForKey: rrType];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[_infoLock unlock];
|
|
[NSException raise: @"NSInternalInconsistencyException"
|
|
format: @"Invalid value of NSNetService info key %@", rrType];
|
|
return;
|
|
}
|
|
|
|
// Special case for address records:
|
|
if ((code == AVAHI_DNS_TYPE_A) || code == AVAHI_DNS_TYPE_AAAA)
|
|
{
|
|
AvahiProtocol proto = AVAHI_PROTO_UNSPEC;
|
|
if (code == AVAHI_DNS_TYPE_A)
|
|
{
|
|
proto = AVAHI_PROTO_INET;
|
|
}
|
|
else if (code == AVAHI_DNS_TYPE_AAAA)
|
|
{
|
|
proto = AVAHI_PROTO_INET6;
|
|
}
|
|
[self removeAddressData: NSDataWithAddrDataPortFamilyAndIface(data,
|
|
[self port], proto, _ifIndex)];
|
|
}
|
|
[_infoLock unlock];
|
|
}
|
|
|
|
/**
|
|
* Called both by the native timeout mechanism and the Avahi callback to notify
|
|
* the delegate about record data changes.
|
|
*/
|
|
- (void) allForNowForRRCode: (NSUInteger)code
|
|
{
|
|
NSString *rrType = nil;
|
|
NSData *data = nil;
|
|
[_lock lock];
|
|
{
|
|
// Remove a dangling timer if any:
|
|
NSTimer *aTimer = (NSTimer*)NSMapGet(_browserTimeouts,
|
|
(void*)(uintptr_t)code);
|
|
if (aTimer != nil)
|
|
{
|
|
[aTimer invalidate];
|
|
NSMapRemove(_browserTimeouts, (void*)(uintptr_t)code);
|
|
}
|
|
}
|
|
[_lock unlock];
|
|
rrType = NSStringFromRRCode(code);
|
|
data = [self infoObjectForKey: rrType];
|
|
[self netService: self didUpdateRecordData: data
|
|
forRecordType: rrType];
|
|
}
|
|
|
|
#if GS_USE_AVAHI==1
|
|
- (id<NSObject,GSNetServiceDelegate>)delegate
|
|
#else
|
|
- (id<NSObject>)delegate
|
|
#endif
|
|
{
|
|
return _delegate;
|
|
}
|
|
|
|
/**
|
|
* Dispatcher method for error notifications to the delegate.
|
|
*/
|
|
- (void) handleError: (int)errorCode
|
|
forRRCode: (int)RRCode
|
|
{
|
|
[_lock lock];
|
|
if (_serviceState < GSNetServiceResolved)
|
|
{
|
|
[self netService: self
|
|
didNotResolve: [self errorDictWithErrorCode: errorCode]];
|
|
}
|
|
else if (_serviceState <= GSNetServiceRecordBrowsing)
|
|
{
|
|
/* Strangely enough, the Apple documentation does not specify that an
|
|
* error should be raised when monitoring a record fails. Since we are
|
|
* not Apple, we can actually be nice and notify the delegate, if it is
|
|
* interested.
|
|
*/
|
|
[self netService: self
|
|
didNotMonitor: [self errorDictWithErrorCode: errorCode]
|
|
forRecordType: NSStringFromRRCode(RRCode)];
|
|
}
|
|
else if (_serviceState > GSNetServiceRecordBrowsing)
|
|
{
|
|
[self netService: self
|
|
didNotPublish: [self errorDictWithErrorCode: errorCode]];
|
|
}
|
|
[self stopWithError: YES
|
|
forRRCode: RRCode];
|
|
[self avahiClientHandleError: errorCode];
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* Dispatcher method for error notifications to the delegate.
|
|
*/
|
|
- (void) handleError: (int)errorCode
|
|
{
|
|
[self handleError: errorCode
|
|
forRRCode: -1];
|
|
}
|
|
|
|
- (id) recordDataForRecordType: (NSString*)key
|
|
{
|
|
return [self infoObjectForKey: key];
|
|
}
|
|
|
|
- (NSData*) TXTRecordData
|
|
{
|
|
id retVal = nil;
|
|
id value = [self infoObjectForKey: @"TXT"];
|
|
//Retain because somebody might remove it while we do this:
|
|
[value retain];
|
|
if ([value isKindOfClass: [NSData class]])
|
|
{
|
|
return [value autorelease];
|
|
}
|
|
else if ([value isKindOfClass: [NSArray class]])
|
|
{
|
|
// Only return the last (= newest) object:
|
|
retVal = [[(NSArray*)value lastObject] retain];
|
|
}
|
|
[value release];
|
|
return [retVal autorelease];
|
|
}
|
|
|
|
- (BOOL) setTXTRecordData: (NSData*)data
|
|
{
|
|
AvahiStringList *list = NULL;
|
|
int ret = GSAvahiStringListFromNSData(data, &list);
|
|
if (0 == ret)
|
|
{
|
|
// We could successfully parse it, so we set it as the value of the
|
|
// dictionary.
|
|
[self setInfoObject: data
|
|
forKey: @"TXT"];
|
|
}
|
|
[_lock lock];
|
|
switch (_serviceState)
|
|
{
|
|
case (GSNetServiceIdle):
|
|
// We don't need to do anything more, the TXT will be published along
|
|
// with the service:
|
|
if (list != NULL)
|
|
{
|
|
avahi_string_list_free(list);
|
|
}
|
|
[_lock unlock];
|
|
return YES;
|
|
case (GSNetServiceResolving):
|
|
case (GSNetServiceResolved):
|
|
case (GSNetServiceRecordBrowsing):
|
|
case GSNetServiceStateMax:
|
|
// TODO: Raise error?
|
|
if (list != NULL)
|
|
{
|
|
avahi_string_list_free(list);
|
|
}
|
|
[_lock unlock];
|
|
return NO;
|
|
case (GSNetServicePublishing):
|
|
case (GSNetServicePublished):
|
|
break;
|
|
}
|
|
|
|
// Ret will still be 0 at this point, so we can reuse the variable:
|
|
ret = avahi_entry_group_update_service_txt_strlst(
|
|
(AvahiEntryGroup*)_entryGroup,
|
|
_ifIndex,
|
|
_protocol,
|
|
0, //no flags
|
|
[[self infoObjectForKey: @"name"] UTF8String],
|
|
[[self infoObjectForKey: @"type"] UTF8String],
|
|
StringOrNullIfEmpty([self infoObjectForKey: @"domain"]),
|
|
list);
|
|
if (list != NULL)
|
|
{
|
|
avahi_string_list_free(list);
|
|
}
|
|
if (ret != 0)
|
|
{
|
|
// TODO: Raise error:
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
/*
|
|
* FIXME: Apple's NSNetService API is a bit crappy, meaning that almost
|
|
* everything is asynchronous, but not -setTXTRecordData:. One
|
|
* solution would be to actually be asynchronous and run the runloop for
|
|
* ourselves a bit. Unfortunately Avahi does not call any of its
|
|
* callbacks to inform us about the fact that the record has
|
|
* successfully been changed. So we are a bit out of luck. Right now,
|
|
* this means that we might be returning 'YES' wrongly and also break
|
|
* code that _expects_ the record to be published before the method
|
|
* returns. Life sucks sometimes.
|
|
*/
|
|
[_lock unlock];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL) addRecordData: (NSData*)data
|
|
forRecordType: (NSString*)type
|
|
withTTL: (NSUInteger)ttl
|
|
{
|
|
int rrCode = RRCodeFromNSString(type);
|
|
int ret = 0;
|
|
[_lock lock];
|
|
if (_serviceState == GSNetServiceIdle)
|
|
{
|
|
if (NULL == _entryGroup)
|
|
{
|
|
int ret = [self createEntryGroup];
|
|
if (ret != 0)
|
|
{
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
|
|
if (!rrCode)
|
|
{
|
|
//FIXME: Raise error.
|
|
[self handleError: NSNetServicesBadArgumentError];
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
|
|
// NOTE: This function is crazly a bit different than the others: The name
|
|
// parameter is the full service name!
|
|
ret = avahi_entry_group_add_record((AvahiEntryGroup*)_entryGroup,
|
|
_ifIndex,
|
|
_protocol,
|
|
0,
|
|
[[self fullServiceName] UTF8String],
|
|
AVAHI_DNS_CLASS_IN, // DNS class
|
|
rrCode,
|
|
ttl,
|
|
[data bytes],
|
|
[data length]);
|
|
if (ret != 0)
|
|
{
|
|
[self handleError: ret];
|
|
[_lock unlock];
|
|
return NO;
|
|
}
|
|
[_lock unlock];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL) addRecordData: (NSData*)data
|
|
forRecordType: (NSString*)type
|
|
{
|
|
return [self addRecordData: data
|
|
forRecordType: type
|
|
withTTL: DEFAULT_OTHER_RECORD_TTL];
|
|
}
|
|
|
|
/**
|
|
* Private method called upon state changes in the entry group.
|
|
*/
|
|
- (void) entryGroup: (AvahiEntryGroup*)group
|
|
enteredState: (AvahiEntryGroupState)groupState
|
|
{
|
|
[_lock lock];
|
|
if (_serviceState == GSNetServicePublishing)
|
|
{
|
|
switch (groupState)
|
|
{
|
|
case (AVAHI_ENTRY_GROUP_UNCOMMITED):
|
|
case (AVAHI_ENTRY_GROUP_REGISTERING):
|
|
break;
|
|
case (AVAHI_ENTRY_GROUP_COLLISION):
|
|
[self handleError: NSNetServicesCollisionError];
|
|
break;
|
|
case (AVAHI_ENTRY_GROUP_FAILURE):
|
|
[self handleError: avahi_client_errno((AvahiClient*)_client)];
|
|
break;
|
|
case (AVAHI_ENTRY_GROUP_ESTABLISHED):
|
|
_serviceState = GSNetServicePublished;
|
|
[self netServiceDidPublish: self];
|
|
break;
|
|
}
|
|
}
|
|
else if (_serviceState == GSNetServicePublished)
|
|
{
|
|
switch (groupState)
|
|
{
|
|
case AVAHI_ENTRY_GROUP_COLLISION:
|
|
case AVAHI_ENTRY_GROUP_FAILURE:
|
|
[self handleError: avahi_client_errno((AvahiClient*)_client)];
|
|
case AVAHI_ENTRY_GROUP_ESTABLISHED:
|
|
case AVAHI_ENTRY_GROUP_UNCOMMITED:
|
|
case AVAHI_ENTRY_GROUP_REGISTERING:
|
|
break;
|
|
}
|
|
}
|
|
[_lock unlock];
|
|
}
|
|
|
|
/**
|
|
* GSServerStream delegate method, called only when this service has been
|
|
* published with the NSNetServiceListenForConnections option.
|
|
*/
|
|
- (void) stream:(NSStream*) stream handleEvent: (NSStreamEvent)anEvent
|
|
{
|
|
switch (anEvent)
|
|
{
|
|
case NSStreamEventHasBytesAvailable:
|
|
{
|
|
if ([[self delegate]
|
|
respondsToSelector:
|
|
@selector(netService:didAcceptConnectionWithInputStream:outputStream:)])
|
|
{
|
|
NSInputStream *is;
|
|
NSOutputStream *os;
|
|
GSServerStream *serverStream = [self infoObjectForKey: @"serverStream"];
|
|
[serverStream acceptWithInputStream: &is outputStream: &os];
|
|
[[self delegate] netService: self
|
|
didAcceptConnectionWithInputStream: is
|
|
outputStream: os];
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
/*
|
|
* Obtain the super-class lock so that nothing fishy can happen while
|
|
* we clean up:
|
|
*/
|
|
[_lock lock];
|
|
|
|
/*
|
|
* Unset the delegate. We might have been gone away because the delegate
|
|
* didn't need us anymore, so there's a reasonable chance that it has also
|
|
* been deallocated.
|
|
*/
|
|
[self setDelegate: nil];
|
|
|
|
/*
|
|
* Call -stop to cleanup all avahi-related resources.
|
|
*/
|
|
[self stop];
|
|
|
|
/* Clean up evrything else. */
|
|
NSFreeMapTable(_browsers);
|
|
_browsers = NULL;
|
|
NSFreeMapTable(_browserTimeouts);
|
|
_browserTimeouts = NULL;
|
|
[_info release];
|
|
_info = nil;
|
|
[_infoLock release];
|
|
_infoLock = nil;
|
|
[_lock unlock];
|
|
[self avahiClientDealloc];
|
|
[super dealloc];
|
|
}
|
|
@end
|