/* Copyright (c) 1995-1999 NEC USA, Inc.  All rights reserved.               */
/*                                                                           */
/* The redistribution, use and modification in source or binary forms of     */
/* this software is subject to the conditions set forth in the copyright     */
/* document ("Copyright") included with this distribution.                   */

/*
 * $Id: hostname.c,v 1.31.4.12 1999/08/02 14:52:23 wlu Exp $
 */

#include "socks5p.h"
#include "protocol.h"
#include "addr.h"
#include "wrap.h"
#include "log.h"

#define S5_HOSTLIST_SIZE    256
#define S5_HOSTALIASES_SIZE 16
#define S5_FAKEHOSTFILE     ".s5fakehost"
 
struct hostEntry {
    char name[S5_HOSTNAME_SIZE];
};

int lsInWrapHostname = 0;
static struct hostEntry *hostnames = NULL;
static int inited = 0;
static int fd = 0;

/* local array of host address list and aliases list                         */
static struct in_addr host_addr[S5_HOSTLIST_SIZE];
static char host_aliases[S5_HOSTALIASES_SIZE][S5_HOSTNAME_SIZE];

/* The address of unresolved host will be of the format "0.0.0.i", where i   */
/* ranges from 1 to 254, pretty simple...                                    */
#define	FAKEPREFIX	"0.0.0."

static int SetReadLock(int lock) {
    struct flock locks;

    locks.l_type = (lock?F_RDLCK:F_UNLCK);
    locks.l_start = 0;
    locks.l_whence = SEEK_SET;
    locks.l_len = 0;

    return fcntl(fd, F_SETLKW, &locks);
}

static int SetWriteLock(int lock) {
    struct flock locks;

    locks.l_type = (lock?F_WRLCK:F_UNLCK);
    locks.l_start = 0;
    locks.l_whence = SEEK_SET;
    locks.l_len = 0;

    return fcntl(fd, F_SETLKW, &locks);
}

#define S_ISRW(mode) (((mode)&(S_IREAD | S_IWRITE)) == (S_IREAD | S_IWRITE))

static int HostFileInit(void) {
    int i, j, flags;
    struct stat sbuf;
    char filename[1024], hostname[S5_HOSTNAME_SIZE];

    sprintf(filename, "/tmp/.s5fakehost.%d", (int)getuid());
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(20), 0, "HostFileInit: fakehost file is %s", filename);

    if (!lstat(filename, &sbuf)) {
	if (sbuf.st_size != 255*sizeof(hostname)+sizeof(int) || !S_ISRW(sbuf.st_mode)) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "HostFileInit: fakehost file (%s) has been changed", filename);
	    flags = O_RDWR | O_CREAT | O_TRUNC | O_EXCL;
	} else if (S_ISLNK(sbuf.st_mode) && geteuid() != sbuf.st_uid) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "HostFileInit: fakehost file (%s) is a link", filename);
	    flags = O_RDWR | O_CREAT | O_TRUNC | O_EXCL;
	} else flags = O_RDWR;
    } else flags = O_RDWR | O_CREAT | O_TRUNC | O_EXCL;

    while ((fd = open(filename, flags, 0600)) < 0) {
	if (errno != EINTR && errno != EAGAIN) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "HostFileInit: open fakehost file (%s) failed %m", filename);
	    return -1;
	}
    }

    if (!(flags & O_CREAT)) return 0;

    SetWriteLock(1);
    i = htonl(0);
    j = sizeof(int);
    lseek(fd, 0, SEEK_SET);
    if (REAL(write)(fd, (char *)&i, j) != j) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "HostFileInit: write fakehost file failed %m");
        REAL(close)(fd);
        fd = -1;
        return -1;
    }

    memset(hostname, 0, sizeof(hostname));
    for (i = 0; i < 255; i++) {
        if (REAL(write)(fd, hostname, sizeof(hostname)) != sizeof(hostname)) {
            S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "HostFileInit: write fakehost file failed %m");
            REAL(close)(fd);
            fd = -1;
            return -1;
        }
    }
    SetWriteLock(0);

    return 0;
}

static int FakeHostInit(void) {
    if (inited != 0) return (inited > 0)?0:-1;

    if (HostFileInit() < 0) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "FakeHostInit: can't use file");
	if (!(hostnames = (struct hostEntry *)calloc(256, sizeof(struct hostEntry)))) {
	    S5LogUpdate(S5LogDefaultHandle, S5_LOG_WARNING, 0, "FakeHostInit: can't create table: %m");
	    inited = -1;
	    return -1;
	}
    }

    inited = 1;
    return 0;
}

static int GetHostFromFile(const char *name) {
    int i, j;
    char hostname[S5_HOSTNAME_SIZE];

    SetWriteLock(1);
    lseek(fd, 0, SEEK_SET);
    i = 0;
    if (REAL(read)(fd, (char *)&i, sizeof(int)) != sizeof(int)) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "GetHostFromFile: read table failed %m");
        SetWriteLock(0);
        return -1;
    }

    i = ntohl(i);
    for (j = 1; j < 256; j++) {
        if (REAL(read)(fd, hostname, sizeof(hostname)) != sizeof(hostname)) {
            S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "GetHostFromFile: read table failed %m");
            SetWriteLock(0);
            return -1;
        }

        if (hostname[0] == '\0' || !strcasecmp(name, hostname)) break;
    }

    if (j == 256 || hostname[0] == '\0') {
        i++;
        if (i == 256) {
            S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(20), 0, "GetHostFromFile: FAKE table is recycled");
            i = 1;
            j = i;
        } else j = i;

	i = htonl(i);
        lseek(fd, 0, SEEK_SET);
        if (REAL(write)(fd, (char *)&i, sizeof(int)) != sizeof(int)) {
            S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "GetHostFromFile: write table failed %m");
            SetWriteLock(0);
            return -1;
        }

        memset(hostname, 0, sizeof(hostname));
        strncpy(hostname, name, MIN(strlen(name), S5_HOSTNAME_SIZE-1));
        hostname[MIN(strlen(name), S5_HOSTNAME_SIZE-1)] = '\0';

        lseek(fd, (j-1)*256+sizeof(int), SEEK_SET);
        if (REAL(write)(fd, hostname, sizeof(hostname)) != sizeof(hostname)) {
            S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "GetHostFromFile: write table failed %m");
            SetWriteLock(0);
            return -1;
        }
    }

    SetWriteLock(0);
    return j;
}

static int GetFakeHost(const char *name) {
    static int i = 0;
    int j;

    if (FakeHostInit() < 0) return -1;

    if (!hostnames) return GetHostFromFile(name);

    for (j = 0; j < 255; j++) {
        if (hostnames[j].name[0] == '\0' || !strcasecmp(name, hostnames[j].name)) break;
    }

    if (j == 255 || hostnames[j].name[0] == '\0') {
	strncpy(hostnames[i].name, name, MIN(strlen(name), S5_HOSTNAME_SIZE-1));
	hostnames[i].name[MIN(strlen(name), S5_HOSTNAME_SIZE-1)] = '\0';
	j = i;
	if (++i == 255) i = 0;
    }

    return (j + 1);
}

static void HostentCopy(struct in_addr **addr_list, char **aliases, const struct hostent *h) {
    int i;

    for (i = 0; i < S5_HOSTALIASES_SIZE; i++) {
	if (h->h_aliases[i] == NULL) break;

	strncpy(host_aliases[i], h->h_aliases[i], MIN(strlen(h->h_aliases[i]), S5_HOSTNAME_SIZE-1));
	host_aliases[i][MIN(strlen(h->h_aliases[i]), S5_HOSTNAME_SIZE-1)] = '\0';
	aliases[i] = (char *)host_aliases[i];
    }
    aliases[i] = NULL;

    for (i = 0; i < S5_HOSTLIST_SIZE; i++) {
	if (h->h_addr_list[i] == NULL) break;

	memcpy((char *)&host_addr[i], h->h_addr_list[i], sizeof(struct in_addr));
	addr_list[i] = &host_addr[i];
    }
    addr_list[i] = NULL;
}

/* wrapper around the gethostbyname call.                                    */
/* similar to gethostbyname() except for:                                    */
/* *** if gethostbyname() fails, then it returns a pointer to a hostent      */
/*     structure filled with a special value, so that SOCKSxxxxxx() will     */
/*     realize that this host was unresolved and fill in the protocol        */
/*     accordingly...                                                        */
/*                                                                           */
/* returns a pointer to a gethostent structure on success; NULL on failure   */
struct hostent *LIBPREFIX(gethostbyname)(const char *name) {
    static struct in_addr special_addr, *my_addr_list[S5_HOSTLIST_SIZE+1];
    static char	my_name[MAXNAMELEN], *my_aliases[S5_HOSTALIASES_SIZE+1];
    static struct hostent h;
    struct hostent *hp;
    char *local, *fake;
    int hlen, i;

#ifdef FOR_SHARED_LIBRARY
    if (lsInRLDFunctions || lsInWrapFunction || lsInWrapHostname) return REAL(gethostbyname)(name);
#endif

    lsInWrapFunction = 1;
    lsInWrapHostname = 1;
    LIBPREFIX2(init)("libsocks5");
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname: looking up %s", name);

    fake  = getenv("SOCKS5_FAKEALLHOSTS");
    local = getenv("SOCKS5_LOCALDNSONLY");

    if (!fake && (hp = REAL(gethostbyname)(name)) != NULL) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname: REAL: %s", inet_ntoa(*(struct in_addr *)hp->h_addr));

        hlen = MIN(strlen(hp->h_name)+1, sizeof(my_name));
        strncpy(my_name, hp->h_name, hlen);
        if (hlen == sizeof(my_name)) my_name[hlen-1] = '\0';

	HostentCopy(my_addr_list, my_aliases, hp);

    	h.h_name      = my_name;
    	h.h_aliases   = my_aliases;
    	h.h_addrtype  = hp->h_addrtype;
    	h.h_length    = hp->h_length;
    	h.h_addr_list = (char **)my_addr_list;
	
        lsInWrapFunction = 0;
        lsInWrapHostname = 0;
	return &h;
    }

    /* If your DNS is the same as the socks server, don't fake a correct     */
    /* lookup when you know it won't work...                                 */
    if (local) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname: REAL: Fake not configured");
        lsInWrapFunction = 0;
        lsInWrapHostname = 0;
	return NULL;
    }

    /* Fill in some UNRESOLVED values and let the daemon resolve it          */
    if ((i = GetFakeHost(name)) <= 0) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "SOCKS gethostbyname: Get fake host failed");
        lsInWrapFunction = 0;
        lsInWrapHostname = 0;
        return NULL;
    }

    hlen = MIN(strlen(name)+1, sizeof(my_name));
    strncpy(my_name, name, hlen);
    if (hlen == sizeof(my_name)) my_name[hlen-1] = '\0';
    my_aliases[0] = NULL;

    special_addr.s_addr = htonl(i);
    my_addr_list[0] = &special_addr;
    my_addr_list[1] = NULL;
	
    h.h_name      = my_name;
    h.h_aliases   = my_aliases;
    h.h_addrtype  = AF_INET;
    h.h_length    = sizeof(struct in_addr);
    h.h_addr_list = (char **)my_addr_list;
    
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname: FAKE: %s",  inet_ntoa(*(struct in_addr *)h.h_addr_list[0]));
    lsInWrapFunction = 0;
    lsInWrapHostname = 0;
    return &h;
}

#ifdef HAVE_GETHOSTBYNAME2
/* wrapper around the gethostbyname2 call.                                   */
/* similar to gethostbyname2() except for:                                   */
/* *** if gethostbyname2() fails, then it returns a pointer to a hostent     */
/*     structure filled with a special value, so that SOCKSxxxxxx() will     */
/*     realize that this host was unresolved and fill in the protocol        */
/*     accordingly...                                                        */
/*                                                                           */
/* returns a pointer to a gethostent structure on success; NULL on failure   */
struct hostent *LIBPREFIX(gethostbyname2)(const char *name, int af) {
    static struct in_addr special_addr, *my_addr_list[S5_HOSTLIST_SIZE+1];
    static char	my_name[MAXNAMELEN], *my_aliases[S5_HOSTALIASES_SIZE+1];
    static struct hostent h;
    struct hostent *hp;
    char *local, *fake;
    int hlen, i;

#ifdef FOR_SHARED_LIBRARY
    if (lsInRLDFunctions || lsInWrapFunction || lsInWrapHostname) return REAL(gethostbyname2)(name, af);
#endif

    if (af != AF_INET) return REAL(gethostbyname2)(name, af);

    lsInWrapFunction = 1;
    lsInWrapHostname = 1;
    LIBPREFIX2(init)("libsocks5");
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname2: looking up %s", name);

    fake  = getenv("SOCKS5_FAKEALLHOSTS");
    local = getenv("SOCKS5_LOCALDNSONLY");

    if (!fake && (hp = REAL(gethostbyname2)(name, af)) != NULL) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname2: REAL: %s", inet_ntoa(*(struct in_addr *)hp->h_addr));

        hlen = MIN(strlen(hp->h_name)+1, sizeof(my_name));
        strncpy(my_name, hp->h_name, hlen);
        if (hlen == sizeof(my_name)) my_name[hlen-1] = '\0';

	HostentCopy(my_addr_list, my_aliases, hp);

    	h.h_name      = my_name;
    	h.h_aliases   = my_aliases;
    	h.h_addrtype  = hp->h_addrtype;
    	h.h_length    = hp->h_length;
    	h.h_addr_list = (char **)my_addr_list;
	
        lsInWrapFunction = 0;
        lsInWrapHostname = 0;
	return &h;
    }

    /* If your DNS is the same as the socks server, don't fake a correct     */
    /* lookup when you know it won't work...                                 */
    if (local) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname2: REAL: Fake not configured");
        lsInWrapFunction = 0;
        lsInWrapHostname = 0;
	return NULL;
    }

    /* Fill in some UNRESOLVED values and let the daemon resolve it          */
    if ((i = GetFakeHost(name)) <= 0) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "SOCKS gethostbyname2: Get fake host failed");
        lsInWrapFunction = 0;
        lsInWrapHostname = 0;
        return NULL;
    }

    hlen = MIN(strlen(name)+1, sizeof(my_name));
    strncpy(my_name, name, hlen);
    if (hlen == sizeof(my_name)) my_name[hlen-1] = '\0';
    my_aliases[0] = NULL;

    special_addr.s_addr = htonl(i);
    my_addr_list[0] = &special_addr;
    my_addr_list[1] = NULL;
	
    h.h_name      = my_name;
    h.h_aliases   = my_aliases;
    h.h_addrtype  = AF_INET;
    h.h_length    = sizeof(struct in_addr);
    h.h_addr_list = (char **)my_addr_list;
    
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "SOCKS gethostbyname2: FAKE: %s",  inet_ntoa(*(struct in_addr *)h.h_addr_list[0]));
    lsInWrapFunction = 0;
    lsInWrapHostname = 0;
    return &h;
}
#endif

int lsGetCachedAddress(const char *name, S5NetAddr *na) {
    int i;
    char hostname[S5_HOSTNAME_SIZE];

    if (inited <= 0) return -1;
    if (!na || !name || !*name) return -1;

    if (fd > 0) {
        SetReadLock(1);
        lseek(fd, sizeof(int), SEEK_SET);

        for (i = 1; i < 256; i++) {
            if (REAL(read)(fd, hostname, sizeof(hostname)) != sizeof(hostname)) {
                S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "lsGetCachedAddress: read fake table failed %m");
                SetReadLock(0);
                return -1;
            }

            if (hostname[0] == '\0') {
                SetReadLock(0);
                return -1;
            }

            if (!strcasecmp(name, hostname)) break;
	}

	SetReadLock(0);
    } else {
        for (i = 0; i < 255; i++) {
	    if (hostnames[i].name[0] == '\0') return -1;
            if (!strcasecmp(name, hostnames[i].name)) break;
	}

	if (i < 255) strcpy(hostname, hostnames[i].name);
	++i;
    }

    if (i < 256) {
	S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(20), 0, "lsGetCachedAddress: Faked host #%d, name is: %s", i, hostname);
	memset(&na->sin, 0, sizeof(ssi));
	na->sin.sin_family      = AF_INET;
	na->sin.sin_port        = 0;
	na->sin.sin_addr.s_addr = htonl(i);
	return 0;
    }

    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(10), 0, "lsGetCachedAddress: Not a fake a hostname: %s", name);
    return -1;
}

/* checks if the address is an unresolved-address and if so, returns the     */
/* unresolved hostname else returns NULL                                     */
int lsGetCachedHostname(const S5NetAddr *na, char *hostname, int len) {
    int i;

    if (inited <= 0) return -1;
    if (!na || !hostname) return -1;

    if (na->sin.sin_family != AF_INET) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(20), 0, "lsGetCachedHostname: Not a fake address, wrong address family: %d", na->sin.sin_family);
        return -1;
    }

    if ((i = (int)ntohl(na->sin.sin_addr.s_addr)) > 255 || i < 1) {
        S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(20), 0, "lsGetCachedHostname: Not a fake hostname: %s", inet_ntoa(na->sin.sin_addr));
        return -1;
    }

    if (fd > 0) {
        SetReadLock(1);
        lseek(fd, (i-1)*256+sizeof(int), SEEK_SET);

        if (REAL(read)(fd, hostname, len) != len) {
            S5LogUpdate(S5LogDefaultHandle, S5_LOG_ERROR, 0, "lsGetCachedHostname: read fake table failed %m");
            SetReadLock(0);
            return -1;
        }
        SetReadLock(0);

        if (!*hostname) return -1;
    } else {
	if (hostnames[i-1].name[0] == '\0') return -1;
	strncpy(hostname, hostnames[i-1].name, MIN(strlen(hostnames[i-1].name)+1, len));
    }

    hostname[len - 1] = '\0';
    S5LogUpdate(S5LogDefaultHandle, S5_LOG_DEBUG(20), 0, "lsGetCachedHostname: Faked host #%d, name is: %s", i, hostname);
    return 0;
}
