blob: b4e041404112379c9aa6aa5b03dde0d9ca4c8827 [file] [log] [blame]
/* -*- Mode: C; tab-width: 4 -*-
*
* Copyright (c) 2002-2003 Apple Computer, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Cocoa/Cocoa.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <unistd.h>
#include <dns_sd.h>
@class ServiceController; // holds state corresponding to outstanding DNSServiceRef
@interface BrowserController : NSObject
{
IBOutlet id nameField;
IBOutlet id typeField;
IBOutlet id serviceDisplayTable;
IBOutlet id typeColumn;
IBOutlet id nameColumn;
IBOutlet id serviceTypeField;
IBOutlet id serviceNameField;
IBOutlet id hostField;
IBOutlet id ipAddressField;
IBOutlet id ip6AddressField;
IBOutlet id portField;
IBOutlet id interfaceField;
IBOutlet id textField;
NSMutableArray *_srvtypeKeys;
NSMutableArray *_srvnameKeys;
NSMutableArray *_sortedServices;
NSMutableDictionary *_servicesDict;
ServiceController *_serviceBrowser;
ServiceController *_serviceResolver;
ServiceController *_ipv4AddressResolver;
ServiceController *_ipv6AddressResolver;
}
- (void)notifyTypeSelectionChange:(NSNotification*)note;
- (void)notifyNameSelectionChange:(NSNotification*)note;
- (IBAction)connect:(id)sender;
- (IBAction)handleTableClick:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)addNewService:(id)sender;
- (IBAction)update:(NSString *)Type;
- (void)updateBrowseWithName:(const char *)name type:(const char *)resulttype domain:(const char *)domain interface:(uint32_t)interface flags:(DNSServiceFlags)flags;
- (void)resolveClientWitHost:(NSString *)host port:(uint16_t)port interfaceIndex:(uint32_t)interface txtRecord:(const char*)txtRecord txtLen:(uint16_t)txtLen;
- (void)updateAddress:(uint16_t)rrtype addr:(const void *)buff addrLen:(uint16_t)addrLen host:(const char*)host interfaceIndex:(uint32_t)interface more:(boolean_t)moreToCome;
- (void)_cancelPendingResolve;
- (void)_clearResolvedInfo;
@end
// The ServiceController manages cleanup of DNSServiceRef & runloop info for an outstanding request
@interface ServiceController : NSObject
{
DNSServiceRef fServiceRef;
CFSocketRef fSocketRef;
CFRunLoopSourceRef fRunloopSrc;
}
- (id)initWithServiceRef:(DNSServiceRef)ref;
- (void)addToCurrentRunLoop;
- (DNSServiceRef)serviceRef;
- (void)dealloc;
@end // interface ServiceController
static void
ProcessSockData(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
DNSServiceRef serviceRef = (DNSServiceRef)info;
DNSServiceErrorType err = DNSServiceProcessResult(serviceRef);
if (err != kDNSServiceErr_NoError) {
printf("DNSServiceProcessResult() returned an error! %d\n", err);
}
}
static void
ServiceBrowseReply(DNSServiceRef sdRef, DNSServiceFlags servFlags, uint32_t interfaceIndex, DNSServiceErrorType errorCode,
const char *serviceName, const char *regtype, const char *replyDomain, void *context)
{
if (errorCode == kDNSServiceErr_NoError) {
[(BrowserController*)context updateBrowseWithName:serviceName type:regtype domain:replyDomain interface:interfaceIndex flags:servFlags];
} else {
printf("ServiceBrowseReply got an error! %d\n", errorCode);
}
}
static void
ServiceResolveReply(DNSServiceRef sdRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode,
const char *fullname, const char *hosttarget, uint16_t port, uint16_t txtLen, const char *txtRecord, void *context)
{
if (errorCode == kDNSServiceErr_NoError) {
[(BrowserController*)context resolveClientWitHost:[NSString stringWithUTF8String:hosttarget] port:port interfaceIndex:interfaceIndex txtRecord:txtRecord txtLen:txtLen];
} else {
printf("ServiceResolveReply got an error! %d\n", errorCode);
}
}
static void
QueryRecordReply(DNSServiceRef DNSServiceRef, DNSServiceFlags flags, uint32_t interfaceIndex, DNSServiceErrorType errorCode,
const char *fullname, uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata, uint32_t ttl, void *context)
{
if (errorCode == kDNSServiceErr_NoError) {
[(BrowserController*)context updateAddress:rrtype addr:rdata addrLen:rdlen host:fullname interfaceIndex:interfaceIndex more:(flags & kDNSServiceFlagsMoreComing)];
} else {
printf("QueryRecordReply got an error! %d\n", errorCode);
}
}
static void
InterfaceIndexToName(uint32_t interface, char *interfaceName)
{
assert(interfaceName);
if (interface == kDNSServiceInterfaceIndexAny) {
// All active network interfaces.
strlcpy(interfaceName, "all", IF_NAMESIZE);
} else if (interface == kDNSServiceInterfaceIndexLocalOnly) {
// Only available locally on this machine.
strlcpy(interfaceName, "local", IF_NAMESIZE);
} else if (interface == kDNSServiceInterfaceIndexP2P) {
// Peer-to-peer.
strlcpy(interfaceName, "p2p", IF_NAMESIZE);
} else {
// Converts interface index to interface name.
if_indextoname(interface, interfaceName);
}
}
@implementation BrowserController //Begin implementation of BrowserController methods
- (void)registerDefaults
{
NSMutableDictionary *regDict = [NSMutableDictionary dictionary];
NSArray *typeArray = [NSArray arrayWithObjects:@"_afpovertcp._tcp",
@"_smb._tcp",
@"_rfb._tcp",
@"_ssh._tcp",
@"_ftp._tcp",
@"_http._tcp",
@"_printer._tcp",
@"_ipp._tcp",
@"_airport._tcp",
@"_presence._tcp",
@"_daap._tcp",
@"_dpap._tcp",
nil];
NSArray *nameArray = [NSArray arrayWithObjects:@"AppleShare Servers",
@"Windows Sharing",
@"Screen Sharing",
@"Secure Shell",
@"FTP Servers",
@"Web Servers",
@"LPR Printers",
@"IPP Printers",
@"AirPort Base Stations",
@"iChat Buddies",
@"iTunes Libraries",
@"iPhoto Libraries",
nil];
[regDict setObject:typeArray forKey:@"SrvTypeKeys"];
[regDict setObject:nameArray forKey:@"SrvNameKeys"];
[[NSUserDefaults standardUserDefaults] registerDefaults:regDict];
}
- (id)init
{
self = [super init];
if (self) {
_srvtypeKeys = nil;
_srvnameKeys = nil;
_serviceBrowser = nil;
_serviceResolver = nil;
_ipv4AddressResolver = nil;
_ipv6AddressResolver = nil;
_sortedServices = [[NSMutableArray alloc] init];
_servicesDict = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)awakeFromNib
{
[typeField sizeLastColumnToFit];
[nameField sizeLastColumnToFit];
[nameField setDoubleAction:@selector(connect:)];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifyTypeSelectionChange:) name:NSTableViewSelectionDidChangeNotification object:typeField];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifyNameSelectionChange:) name:NSTableViewSelectionDidChangeNotification object:nameField];
_srvtypeKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvTypeKeys"] mutableCopy];
_srvnameKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvNameKeys"] mutableCopy];
if (!_srvtypeKeys || !_srvnameKeys) {
[_srvtypeKeys release];
[_srvnameKeys release];
[self registerDefaults];
_srvtypeKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvTypeKeys"] mutableCopy];
_srvnameKeys = [[[NSUserDefaults standardUserDefaults] arrayForKey:@"SrvNameKeys"] mutableCopy];
}
[typeField reloadData];
}
- (void)dealloc
{
[_srvtypeKeys release];
[_srvnameKeys release];
[_servicesDict release];
[_sortedServices release];
[super dealloc];
}
-(void)tableView:(NSTableView *)theTableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(int)row
{
if (row < 0) return;
}
- (int)numberOfRowsInTableView:(NSTableView *)theTableView //Begin mandatory TableView methods
{
if (theTableView == typeField) {
return [_srvnameKeys count];
}
if (theTableView == nameField) {
return [_servicesDict count];
}
if (theTableView == serviceDisplayTable) {
return [_srvnameKeys count];
}
return 0;
}
- (id)tableView:(NSTableView *)theTableView objectValueForTableColumn:(NSTableColumn *)theColumn row:(int)rowIndex
{
if (theTableView == typeField) {
return [_srvnameKeys objectAtIndex:rowIndex];
}
if (theTableView == nameField) {
return [[_servicesDict objectForKey:[_sortedServices objectAtIndex:rowIndex]] name];
}
if (theTableView == serviceDisplayTable) {
if (theColumn == typeColumn) {
return [_srvtypeKeys objectAtIndex:rowIndex];
}
if (theColumn == nameColumn) {
return [_srvnameKeys objectAtIndex:rowIndex];
}
return nil;
}
return nil;
}
- (void)notifyTypeSelectionChange:(NSNotification*)note
{
[self _cancelPendingResolve];
int index = [[note object] selectedRow];
if (index != -1) {
[self update:[_srvtypeKeys objectAtIndex:index]];
} else {
[self update:nil];
}
}
- (void)notifyNameSelectionChange:(NSNotification*)note
{
[self _cancelPendingResolve];
int index = [[note object] selectedRow];
if (index == -1) {
return;
}
// Get the currently selected service
NSNetService *service = [_servicesDict objectForKey:[_sortedServices objectAtIndex:index]];
DNSServiceRef serviceRef;
DNSServiceErrorType err = DNSServiceResolve(&serviceRef,
(DNSServiceFlags)0,
kDNSServiceInterfaceIndexAny,
(const char *)[[service name] UTF8String],
(const char *)[[service type] UTF8String],
(const char *)[[service domain] UTF8String],
(DNSServiceResolveReply)ServiceResolveReply,
self);
if (kDNSServiceErr_NoError == err) {
_serviceResolver = [[ServiceController alloc] initWithServiceRef:serviceRef];
[_serviceResolver addToCurrentRunLoop];
}
}
- (IBAction)update:(NSString *)theType
{
[_servicesDict removeAllObjects];
[_sortedServices removeAllObjects];
[nameField reloadData];
// get rid of the previous browser if one exists
if (_serviceBrowser != nil) {
[_serviceBrowser release];
_serviceBrowser = nil;
}
if (theType) {
DNSServiceRef serviceRef;
DNSServiceErrorType err = DNSServiceBrowse(&serviceRef, (DNSServiceFlags)0, 0, [theType UTF8String], NULL, ServiceBrowseReply, self);
if (kDNSServiceErr_NoError == err) {
_serviceBrowser = [[ServiceController alloc] initWithServiceRef:serviceRef];
[_serviceBrowser addToCurrentRunLoop];
}
}
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
return YES;
}
- (void)updateBrowseWithName:(const char *)name type:(const char *)type domain:(const char *)domain interface:(uint32_t)interface flags:(DNSServiceFlags)flags
{
NSString *key = [NSString stringWithFormat:@"%s.%s%s%d", name, type, domain, interface];
NSNetService *service = [[NSNetService alloc] initWithDomain:[NSString stringWithUTF8String:domain] type:[NSString stringWithUTF8String:type] name:[NSString stringWithUTF8String:name]];
if (flags & kDNSServiceFlagsAdd) {
[_servicesDict setObject:service forKey:key];
} else {
[_servicesDict removeObjectForKey:key];
}
// If not expecting any more data, then reload (redraw) TableView with newly found data
if (!(flags & kDNSServiceFlagsMoreComing)) {
// Save the current TableView selection
int index = [nameField selectedRow];
NSString *selected = (index != -1) ? [[_sortedServices objectAtIndex:index] copy] : nil;
[_sortedServices release];
_sortedServices = [[_servicesDict allKeys] mutableCopy];
[_sortedServices sortUsingSelector:@selector(caseInsensitiveCompare:)];
[nameField reloadData];
// Restore the previous TableView selection
index = selected ? [_sortedServices indexOfObject:selected] : NSNotFound;
if (index != NSNotFound) {
[nameField selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
[nameField scrollRowToVisible:index];
}
[selected release];
}
[service release];
return;
}
- (void)resolveClientWitHost:(NSString *)host port:(uint16_t)port interfaceIndex:(uint32_t)interface txtRecord:(const char*)txtRecord txtLen:(uint16_t)txtLen
{
DNSServiceRef serviceRef;
if (_ipv4AddressResolver) {
[_ipv4AddressResolver release];
_ipv4AddressResolver = nil;
}
if (_ipv6AddressResolver) {
[_ipv6AddressResolver release];
_ipv6AddressResolver = nil;
}
// Start an async lookup for IPv4 addresses
DNSServiceErrorType err = DNSServiceQueryRecord(&serviceRef, (DNSServiceFlags)0, interface, [host UTF8String], kDNSServiceType_A, kDNSServiceClass_IN, QueryRecordReply, self);
if (err == kDNSServiceErr_NoError) {
_ipv4AddressResolver = [[ServiceController alloc] initWithServiceRef:serviceRef];
[_ipv4AddressResolver addToCurrentRunLoop];
}
// Start an async lookup for IPv6 addresses
err = DNSServiceQueryRecord(&serviceRef, (DNSServiceFlags)0, interface, [host UTF8String], kDNSServiceType_AAAA, kDNSServiceClass_IN, QueryRecordReply, self);
if (err == kDNSServiceErr_NoError) {
_ipv6AddressResolver = [[ServiceController alloc] initWithServiceRef:serviceRef];
[_ipv6AddressResolver addToCurrentRunLoop];
}
char interfaceName[IF_NAMESIZE];
InterfaceIndexToName(interface, interfaceName);
[hostField setStringValue:host];
[interfaceField setStringValue:[NSString stringWithUTF8String:interfaceName]];
[portField setIntValue:ntohs(port)];
// kind of a hack: munge txtRecord so it's human-readable
if (txtLen > 0) {
char *readableText = (char*) malloc(txtLen);
if (readableText != nil) {
ByteCount index, subStrLen;
memcpy(readableText, txtRecord, txtLen);
for (index=0; index < txtLen - 1; index += subStrLen + 1) {
subStrLen = readableText[index];
readableText[index] = ' ';
}
[textField setStringValue:[NSString stringWithCString:&readableText[1] length:txtLen - 1]];
free(readableText);
}
}
}
- (void)updateAddress:(uint16_t)rrtype addr:(const void *)buff addrLen:(uint16_t)addrLen host:(const char*) host interfaceIndex:(uint32_t)interface more:(boolean_t)moreToCome
{
char addrBuff[256];
if (rrtype == kDNSServiceType_A) {
inet_ntop(AF_INET, buff, addrBuff, sizeof(addrBuff));
if ([[ipAddressField stringValue] length] > 0) {
[ipAddressField setStringValue:[NSString stringWithFormat:@"%@, ", [ipAddressField stringValue]]];
}
[ipAddressField setStringValue:[NSString stringWithFormat:@"%@%s", [ipAddressField stringValue], addrBuff]];
if (!moreToCome) {
[_ipv4AddressResolver release];
_ipv4AddressResolver = nil;
}
} else if (rrtype == kDNSServiceType_AAAA) {
inet_ntop(AF_INET6, buff, addrBuff, sizeof(addrBuff));
if ([[ip6AddressField stringValue] length] > 0) {
[ip6AddressField setStringValue:[NSString stringWithFormat:@"%@, ", [ip6AddressField stringValue]]];
}
[ip6AddressField setStringValue:[NSString stringWithFormat:@"%@%s", [ip6AddressField stringValue], addrBuff]];
if (!moreToCome) {
[_ipv6AddressResolver release];
_ipv6AddressResolver = nil;
}
}
}
- (void)connect:(id)sender
{
NSString *host = [hostField stringValue];
NSString *txtRecord = [textField stringValue];
int port = [portField intValue];
int index = [nameField selectedRow];
NSString *selected = (index >= 0) ? [_sortedServices objectAtIndex:index] : nil;
NSString *type = [[_servicesDict objectForKey:selected] type];
if ([type isEqual:@"_http._tcp."]) {
NSString *pathDelim = @"path=";
NSRange where;
// If the TXT record specifies a path, extract it.
where = [txtRecord rangeOfString:pathDelim options:NSCaseInsensitiveSearch];
if (where.length) {
NSRange targetRange = { where.location + where.length, [txtRecord length] - where.location - where.length };
NSRange endDelim = [txtRecord rangeOfString:@"\n" options:kNilOptions range:targetRange];
if (endDelim.length) // if a delimiter was found, truncate the target range
targetRange.length = endDelim.location - targetRange.location;
NSString *path = [txtRecord substringWithRange:targetRange];
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d%@", host, port, path]]];
} else {
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@:%d", host, port]]];
}
}
else if ([type isEqual:@"_ftp._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"ftp://%@:%d/", host, port]]];
else if ([type isEqual:@"_ssh._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"ssh://%@:%d/", host, port]]];
else if ([type isEqual:@"_afpovertcp._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"afp://%@:%d/", host, port]]];
else if ([type isEqual:@"_smb._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"smb://%@:%d/", host, port]]];
else if ([type isEqual:@"_rfb._tcp."]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"vnc://%@:%d/", host, port]]];
return;
}
- (IBAction)handleTableClick:(id)sender
{
//populate the text fields
}
- (IBAction)removeSelected:(id)sender
{
// remove the selected row and force a refresh
int selectedRow = [serviceDisplayTable selectedRow];
if (selectedRow) {
[_srvtypeKeys removeObjectAtIndex:selectedRow];
[_srvnameKeys removeObjectAtIndex:selectedRow];
[[NSUserDefaults standardUserDefaults] setObject:_srvtypeKeys forKey:@"SrvTypeKeys"];
[[NSUserDefaults standardUserDefaults] setObject:_srvnameKeys forKey:@"SrvNameKeys"];
[typeField reloadData];
[serviceDisplayTable reloadData];
}
}
- (IBAction)addNewService:(id)sender
{
// add new entries from the edit fields to the arrays for the defaults
NSString *newType = [serviceTypeField stringValue];
NSString *newName = [serviceNameField stringValue];
// 3282283: trim trailing '.' from service type field
if ([newType length] && [newType hasSuffix:@"."])
newType = [newType substringToIndex:[newType length] - 1];
if ([newType length] && [newName length]) {
[_srvtypeKeys addObject:newType];
[_srvnameKeys addObject:newName];
[[NSUserDefaults standardUserDefaults] setObject:_srvtypeKeys forKey:@"SrvTypeKeys"];
[[NSUserDefaults standardUserDefaults] setObject:_srvnameKeys forKey:@"SrvNameKeys"];
[typeField reloadData];
[serviceDisplayTable reloadData];
}
}
- (void)_cancelPendingResolve
{
[_ipv4AddressResolver release];
_ipv4AddressResolver = nil;
[_ipv6AddressResolver release];
_ipv6AddressResolver = nil;
[_serviceResolver release];
_serviceResolver = nil;
[self _clearResolvedInfo];
}
- (void)_clearResolvedInfo
{
[hostField setStringValue:@""];
[ipAddressField setStringValue:@""];
[ip6AddressField setStringValue:@""];
[portField setStringValue:@""];
[interfaceField setStringValue:@""];
[textField setStringValue:@""];
}
@end // implementation BrowserController
@implementation ServiceController : NSObject
{
DNSServiceRef fServiceRef;
CFSocketRef fSocketRef;
CFRunLoopSourceRef fRunloopSrc;
}
- (id)initWithServiceRef:(DNSServiceRef)ref
{
self = [super init];
if (self) {
fServiceRef = ref;
fSocketRef = NULL;
fRunloopSrc = NULL;
}
return self;
}
- (void)addToCurrentRunLoop
{
CFSocketContext context = { 0, (void*)fServiceRef, NULL, NULL, NULL };
fSocketRef = CFSocketCreateWithNative(kCFAllocatorDefault, DNSServiceRefSockFD(fServiceRef), kCFSocketReadCallBack, ProcessSockData, &context);
if (fSocketRef) {
// Prevent CFSocketInvalidate from closing DNSServiceRef's socket.
CFOptionFlags sockFlags = CFSocketGetSocketFlags(fSocketRef);
CFSocketSetSocketFlags(fSocketRef, sockFlags & (~kCFSocketCloseOnInvalidate));
fRunloopSrc = CFSocketCreateRunLoopSource(kCFAllocatorDefault, fSocketRef, 0);
}
if (fRunloopSrc) {
CFRunLoopAddSource(CFRunLoopGetCurrent(), fRunloopSrc, kCFRunLoopDefaultMode);
} else {
printf("Could not listen to runloop socket\n");
}
}
- (DNSServiceRef)serviceRef
{
return fServiceRef;
}
- (void)dealloc
{
if (fSocketRef) {
CFSocketInvalidate(fSocketRef); // Note: Also closes the underlying socket
CFRelease(fSocketRef);
// Workaround that gives time to CFSocket's select thread so it can remove the socket from its
// FD set before we close the socket by calling DNSServiceRefDeallocate. <rdar://problem/3585273>
usleep(1000);
}
if (fRunloopSrc) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), fRunloopSrc, kCFRunLoopDefaultMode);
CFRelease(fRunloopSrc);
}
DNSServiceRefDeallocate(fServiceRef);
[super dealloc];
}
@end // implementation ServiceController
int main(int argc, const char *argv[])
{
return NSApplicationMain(argc, argv);
}