// Copyright 1997-2001 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "ONHost.h"

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniBase/system.h>

#import "ONHost-InternalAPI.h"
#import "ONHostAddress.h"

RCS_ID("$Header: /NetworkDisk/Source/CVS/OmniGroup/Frameworks/OmniNetworking/ONHost.m,v 1.29.6.1 2001/06/23 00:01:25 kc Exp $")

@interface ONHost (Private)
- (BOOL)_tryLocalhost;
- (BOOL)_tryToLookupHostInfoAsDottedQuad;
@end

@implementation ONHost

BOOL ONHostNameLookupDebug = NO;
NSRecursiveLock *ONHostLookupLock;

static NSMutableDictionary *hostCache;
static NSString *domainName;
static NSString *localHostname;
static NSTimeInterval ONHostDefaultTimeToLiveTimeInterval = 60.0 * 60.0;

+ (void)didLoad;
{
    // (See <OmniBase/OBPostloader.h> for information about +didLoad methods, especially what you need to call to ensure that they get called.)

    // Calling +self ensures that +initialize gets called early (in particular, before we go multithreaded in OmniWeb).  It might be sufficient to lock the main thread around res_init() in +initialize rather than doing this.

    [self self];
}

+ (void)initialize;
{
    char cLocalHostName[MAXHOSTNAMELEN + 1];
    
    OBINITIALIZE;

    ONHostLookupLock = [[NSRecursiveLock alloc] init];
    hostCache = [[NSMutableDictionary alloc] initWithCapacity:16];

    if (gethostname(cLocalHostName, MAXHOSTNAMELEN) == 0) {
        cLocalHostName[MAXHOSTNAMELEN] = '\0';
        localHostname = [[NSString alloc] initWithCString:cLocalHostName];
    } else {
        localHostname = @"localhost";
    }

    domainName = nil;
#ifdef WIN32
    domainName = [[[self hostForHostname:localHostname] domainName] retain];
#else
    if (!(_res.options & RES_INIT))
        res_init();
    domainName = [[NSString alloc] initWithCString:_res.defdname];
#endif
    if (!domainName || [domainName length] == 0)
        domainName = @"unknown.domain";
}

+ (void)setDebug:(BOOL)newDebugSetting;
{
    ONHostNameLookupDebug = newDebugSetting;
}

+ (NSString *)domainName;
{
    return domainName;
}

+ (NSString *)localHostname;
{
    return localHostname;
}

+ (NSDictionary *)_hostCache;
{
    return hostCache;
}

+ (ONHost *)hostForHostname:(NSString *)aHostname;
{
    ONHost *host;
    NSThread *currentThread = nil;
    NSLock *pendingLock;
    NSException *raisedException = nil;

    if (!aHostname)
	return nil;

    if (ONHostNameLookupDebug) {
        currentThread = [NSThread currentThread];
        NSLog(@"<%@> Starting name lookup for %@", currentThread, aHostname);
    }
    
    [ONHostLookupLock lock];
    
    // First, check under the current capitalization of the address.  Since this is usually correct, this will avoid needlessly creating another string
    if (!(host = [hostCache objectForKey:aHostname])) {
        // Try the lowercase string
        aHostname = [aHostname lowercaseString];
        host = [hostCache objectForKey:aHostname];
    }
    
    while (host != nil) {
        if ([host isKindOfClass:self]) {
            // We're done if we found an non-expired ONHost in the cache
            if ([host isExpired]) {
                if (ONHostNameLookupDebug)
                    NSLog(@"<%@> Flushing expired cache entry %@", currentThread, host);
                [hostCache removeObjectForKey:aHostname];
                host = nil;
            } else {
                if (ONHostNameLookupDebug)
                    NSLog(@"<%@> Request satisfied from cache with %@", currentThread, host);
                [host retain];
                [ONHostLookupLock unlock];
                return [host autorelease];
            }
        } else if ([host isKindOfClass:[NSLock class]]) {
            NSLock *pendingLock;
    
            if (ONHostNameLookupDebug)
                NSLog(@"<%@> Lookup already in progress for %@.  Waiting for it to finish...", currentThread, aHostname);
            
            // Someone is already creating this host!  Hold onto the pendingLock they inserted and let them proceed.
            pendingLock = (NSLock *)host;
            [pendingLock retain];
            [ONHostLookupLock unlock];
    
            // Wait for them to finish.
            [pendingLock lock];
            [pendingLock unlock];
            [pendingLock release];
    
            if (ONHostNameLookupDebug)
                NSLog(@"<%@> Rechecking cache...", currentThread);
            
            // Now look again.
            [ONHostLookupLock lock];
            host = [hostCache objectForKey:aHostname];
        }
    }
    OBASSERT(host == nil);

    // There were either no previous attempts to determine the address for this host name or they failed.  We will unlock the main lock while we process the request so that others can get at the cache and possibly start their own request.  If another thread asks for the same host that we are currently resolving, though, they will need to block.  We will put an lock in the cache under the inquiry name for this purpose.
    pendingLock = [[NSLock alloc] init];
    [hostCache setObject:pendingLock forKey:aHostname];
    [pendingLock lock];

    // Unlock to start the lookup
    [ONHostLookupLock unlock];

    // Do the lookup.  If there is an error we need to remove the pendingLock, unlock it and then reraise so that other threads may try the lookup (we don't cache failures).
    NS_DURING {
        host = [[self alloc] _initWithHostname:aHostname];
    } NS_HANDLER {
        OBASSERT(host == nil);
        raisedException = localException;
    } NS_ENDHANDLER;

    // Lock the cache again now that we have our result (or error)
    [ONHostLookupLock lock];

    if (host)
        [hostCache setObject:host forKey:aHostname];
    else
        [hostCache removeObjectForKey:aHostname];

    // Unlock the pending lock and the main lock
    [pendingLock unlock];
    [pendingLock release];
    [ONHostLookupLock unlock];

    if (raisedException)
        [raisedException raise];

    return [host autorelease];
}

+ (ONHost *)hostForAddress:(ONHostAddress *)anAddress;
{
    struct hostent *hostEntry;

#warning +hostForAddress: is not yet thread-safe
    // This call to gethostbyaddr() is not thread-safe.  We need to do the same sort of stuff as we do to avoid unthreadsafe calls to gethostbyname(), i.e. fork off a tool or call lookupd directly.  Note that the GetHostEntry tool is ready for this sort of use, we just haven't gotten around to writing the code yet.

    // We should also cache the results of this lookup, as we do when we look up hosts by name.
    hostEntry = gethostbyaddr((void *)[anAddress internetAddress], sizeof(struct in_addr), AF_INET);
    if (!hostEntry)
        [self _raiseExceptionForHostErrorNumber:h_errno hostname:[anAddress description]];

    return [self hostForHostname:[NSString stringWithCString:hostEntry->h_name]];
}

+ (void)flushCache;
{
    NSArray *hostnames;
    unsigned int hostnameIndex, hostnameCount;

    [ONHostLookupLock lock];
    NS_DURING {
        if (ONHostNameLookupDebug)
            NSLog(@"<%@> Flushing host cache", [NSThread currentThread]);
        hostnames = [[hostCache keyEnumerator] allObjects];
        hostnameCount = [hostnames count];
        for (hostnameIndex = 0; hostnameIndex < hostnameCount; hostnameIndex++) {
            NSString *aHostname;
            ONHost *host;

            aHostname = [hostnames objectAtIndex:hostnameIndex];
            host = [hostCache objectForKey:aHostname];
            if ([host isKindOfClass:self]) {
                // Only remove the ONHost entries, not the pending locks
                [hostCache removeObjectForKey:aHostname];
            }
        }
    } NS_HANDLER {
        NSLog(@"+[ONHost flushCache]: Warning: %@", [localException reason]);
    } NS_ENDHANDLER;
    [ONHostLookupLock unlock];
}

+ (void)setDefaultTimeToLiveTimeInterval:(NSTimeInterval)newValue;
{
    ONHostDefaultTimeToLiveTimeInterval = newValue;
    [self flushCache];
}

+ (BOOL)isDottedQuadString:(NSString *)string;
{
    unsigned long int address;

    address = inet_addr([string cString]);
    return address != (unsigned long int)-1;
}

- (void)dealloc;
{
    [hostname release];
    [canonicalHostname release];
    [addresses release];
    [expirationDate release];
    [super dealloc];
}

- (NSString *)hostname;
{
    return hostname;
}

- (NSArray *)addresses;
{
    return addresses;
}

- (NSString *)canonicalHostname;
{
    return canonicalHostname;
}

- (NSString *)domainName;
{
    NSRange dotRange;

    dotRange = [canonicalHostname rangeOfString:@"." options:NSLiteralSearch];
    if (dotRange.length == 0)
        return nil;
    return [canonicalHostname substringFromIndex:NSMaxRange(dotRange)];
}

- (void)flushFromHostCache;
{
    [ONHostLookupLock lock];
    NS_DURING {
        if ([hostCache objectForKey:hostname] == self)
            [hostCache removeObjectForKey:hostname];
    } NS_HANDLER {
        NSLog(@"+[ONHost removeFromHostCache]: Warning: %@", [localException reason]);
    } NS_ENDHANDLER;
    [ONHostLookupLock unlock];
}

// Debugging

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];
    if (addresses)
        [debugDictionary setObject:addresses forKey:@"addresses"];
    if (hostname)
        [debugDictionary setObject:hostname forKey:@"hostname"];
    if (expirationDate)
        [debugDictionary setObject:expirationDate forKey:@"expirationDate"];
    return debugDictionary;
}

@end


@implementation ONHost (ONInternalAPI)

+ (void)_raiseExceptionForHostErrorNumber:(int)hostErrorNumber hostname:(NSString *)aHostname;
{
    switch (hostErrorNumber) {
        case HOST_NOT_FOUND:
            [NSException raise:ONHostNotFoundExceptionName format:NSLocalizedStringFromTableInBundle(@"No such host %@", @"OmniNetworking", [NSBundle bundleForClass:[self class]], error), aHostname];
            break;
        case TRY_AGAIN:
            [NSException raise:ONHostNotFoundExceptionName format:NSLocalizedStringFromTableInBundle(@"Temporary error looking up host %@, try again", @"OmniNetworking", [NSBundle bundleForClass:[self class]], error), aHostname];
            break;
        case NO_RECOVERY:
            [NSException raise:ONHostNotFoundExceptionName format:NSLocalizedStringFromTableInBundle(@"Unexpected server failure looking up host %@", @"OmniNetworking", [NSBundle bundleForClass:[self class]], error), aHostname];
            break;
        case NO_DATA:
            [NSException raise:ONHostHasNoAddressesExceptionName format:NSLocalizedStringFromTableInBundle(@"Found no addresses for host %@", @"OmniNetworking", [NSBundle bundleForClass:[self class]], error), aHostname];
            break;
        default:
            [NSException raise:ONHostNameLookupErrorExceptionName format:NSLocalizedStringFromTableInBundle(@"Error looking up host %@", @"OmniNetworking", [NSBundle bundleForClass:[self class]], error), aHostname];
            break;
    }
}

- _initWithHostname:(NSString *)aHostname;
{
    
    if (!(self = [super init]))
	return nil;

    hostname = [aHostname retain];
    addresses = [[NSMutableArray alloc] init];

    if ([self _tryLocalhost] || [self _tryToLookupHostInfoAsDottedQuad])
        return self;

#if defined(ONHOST_USE_THREADSAFE_RESOLVER)
    [self _lookupHostInfoUsingThreadSafeResolver];
#elif defined(ONHOST_USE_THREADSAFE_LOOKUPD_GETHOSTBYNAME)
    [self _lookupHostInfoByLookupDaemon];
#else
    [self _lookupHostInfoByPipe];
#endif
    if (expirationDate == nil)
        expirationDate = [[NSDate alloc] initWithTimeIntervalSinceNow:ONHostDefaultTimeToLiveTimeInterval];


    return self;
}

// The normal gethostbyname() function is not thread-safe.  Additionally, if the name ends up being unregistered, a long timeout period will take effect during which we can do no other host name lookups, assuming that we were to solve this problem by simply putting a lock around our calls to gethostbyname().

// Instead, we have a tool subproject that executes the call to gethostbyname() in its own process.  Under NEXTSTEP and Rhapsody, this will cause two calls to the lookupd process which will in turn contact DNS, NetInfo, NIS and possibly other services (LDAP) in the future in order to resolve the name.  Since lookupd itself is multi-threaded, this will allow two namelookups to proceed in parallel.

// Other operating systems should also support parallel name lookups at this level and if they don't then there probably isn't much that we could do about it.

// This method invokes the ONGetHostByName tool.  If there is an error looking up the addresses for the hostname, it will be returned in the exit status.  Otherwise, the network addresses for the host will be output as raw bytes in the byte order that they would have been returned to us from gethostbyname().  This allows us to easily parse the ints and stick them in a in_addr struct.

- (BOOL)isExpired;
{
    return [expirationDate timeIntervalSinceNow] < 0.0;
}

- (void)_lookupHostInfoByPipe;
{
    static NSString *ONGetHostByNamePath;
    const unsigned long int *addressBytes;
    unsigned long int address, addressCount;
    NSTask *task;
    NSData *outputData, *addressData;
    unsigned int terminationStatus;
    NSPipe *pipe;
    unsigned int canonicalHostnameLength, hostnameLength;
    NSRange nextRange;

    if (!ONGetHostByNamePath) {
        NSString *toolExtension;

#ifdef WIN32
        toolExtension = @"exe";
#else
        toolExtension = @"";
#endif
        ONGetHostByNamePath = [[[NSBundle bundleForClass:isa] pathForResource:@"ONGetHostEntry" ofType:toolExtension] retain];
        if (!ONGetHostByNamePath)
            [NSException raise:ONGetHostByNameNotFoundExceptionName format:@"Cannot find the ONGetHostEntry tool"];
    }

    pipe = [[NSPipe alloc] init];
    task = [[NSTask alloc] init];
    [task setLaunchPath:ONGetHostByNamePath];
    [task setArguments:[NSArray arrayWithObjects:@"name", hostname, nil]];
    [task setStandardOutput:pipe];
    [task launch];

    outputData = [[pipe fileHandleForReading] readDataToEndOfFile];
    [task waitUntilExit];
    terminationStatus = [task terminationStatus];
    [pipe release];
    [task release];

    if (terminationStatus != 0)
        [ONHost _raiseExceptionForHostErrorNumber:terminationStatus hostname:hostname];

    nextRange = NSMakeRange(0, sizeof(canonicalHostnameLength));
    [outputData getBytes:&canonicalHostnameLength range:nextRange];
    hostnameLength = MIN(canonicalHostnameLength, [outputData length] - sizeof(canonicalHostnameLength));
    nextRange = NSMakeRange(NSMaxRange(nextRange), hostnameLength);
    canonicalHostname = [[NSString alloc] initWithData:[outputData subdataWithRange:nextRange] encoding:[NSString defaultCStringEncoding]];
    nextRange = NSMakeRange(NSMaxRange(nextRange), [outputData length] - NSMaxRange(nextRange));
    addressData = [outputData subdataWithRange:nextRange];
    addressCount = [addressData length] / sizeof(address);
    if (addressCount == 0)
        [ONHost _raiseExceptionForHostErrorNumber:NO_DATA hostname:hostname];

    addressBytes = [addressData bytes];
    while (addressCount--) {
        struct in_addr addr;

        addr.s_addr = *addressBytes++;
        [addresses addObject:[ONHostAddress hostAddressWithInternetAddress:&addr]];
    }
}

#ifdef ONHOST_USE_THREADSAFE_RESOLVER

// This looks up host information directly.  This only works on systems where gethostbyname_r exists.

- (void)_lookupHostInfoUsingThreadSafeResolver;
{
    struct hostent *hostEntry, hostEntryBuffer;
    char canonicalHostNameBuffer[1024];
    int hostErrorNumber;
    unsigned int entryIndex;

    hostEntry = gethostbyname_r([hostname cString], &hostEntryBuffer, canonicalHostNameBuffer, sizeof(canonicalHostNameBuffer), &hostErrorNumber);
    if (!hostEntry)
        [ONHost _raiseExceptionForHostErrorNumber:h_errno hostname:hostname];

    canonicalHostname = [[NSString alloc] initWithCString:hostEntry->h_name];
    if (hostEntry->h_addrtype != AF_INET || hostEntry->h_length != sizeof(struct in_addr)) {
        // The host has no IP address.
        return;
    }

    // Store the addresses
    for (entryIndex = 0; hostEntry->h_addr_list[entryIndex]; entryIndex++) {
        ONHostAddress *address;

        address = [ONHostAddress hostAddressWithInternetAddress:(struct in_addr *)hostEntry->h_addr_list[entryIndex]];
        [addresses addObject:address];
    }
}

#endif

@end

@implementation ONHost (Private)

- (BOOL)_tryLocalhost;
    // Make sure localhost works even when the system is misconfigured.
{
    if ([hostname caseInsensitiveCompare:@"localhost"] != NSOrderedSame)
        return NO;

    canonicalHostname = [@"localhost" retain];
    [addresses addObject:[ONHostAddress loopbackAddress]];
    return YES;
}

- (BOOL)_tryToLookupHostInfoAsDottedQuad;
{
    unsigned long int address;
    struct in_addr addr;

    address = inet_addr([hostname cString]);
    if (address == INADDR_NONE)
        return NO;

    // Oh ho!  They gave us an IP number in dotted quad notation!  I guess we'll return the dotted quad as the canonical hostname, and the converted address as the address.
    // (We're not calculating the real canonical hostname because it might return more addresses, and that wouldn't be what the user want since they specifically specified a single address.)
    canonicalHostname = [hostname copy];
    addr.s_addr = address;
    [addresses addObject:[ONHostAddress hostAddressWithInternetAddress:&addr]];
    return YES;
}

@end

DEFINE_NSSTRING(ONHostNotFoundExceptionName);
DEFINE_NSSTRING(ONHostNameLookupErrorExceptionName);
DEFINE_NSSTRING(ONHostHasNoAddressesExceptionName);
DEFINE_NSSTRING(ONGetHostByNameNotFoundExceptionName);
