#include <config.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>

#ifndef WIN32
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>
#define DIRECTORY_SEP "/"
#define IS_DIRECTORY_SEP(c) ((c) == '/')
#else
#define stat _stat
#define chdir _chdir
#define S_ISDIR(m) ((m) & _S_IFDIR)
#define S_IRUSR _S_IREAD
#define S_IWUSR _S_IWRITE
#define DIRECTORY_SEP "\\"
#define IS_DIRECTORY_SEP(c) (((c) == '/') || ((c) == '\\'))
#endif

#ifdef HAVE_UNIX_SOCKET
#ifdef HAVE_STDDEF_H
#include <stddef.h>
#endif
#include <sys/un.h>
#endif

#ifndef WIN32
#include <unistd.h>
#include <dirent.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/resource.h>
#else
// TODO
#endif

#if defined(HAVE_POLL)
#include <sys/poll.h>
#elif defined(HAVE_SELECT)
#include <sys/select.h>
#endif


#include "IMUtil.hh"
#include "IMLog.hh"

#ifdef HAVE_TLS
#include <openssl/ssl.h>
#include "IMTLS.hh"
#endif

/*******************************************************************************
                             DirectoryInfo
*******************************************************************************/

bool
DirectoryInfo::addfile(
    const char *filename
)
{
    struct stat s;
    DirectoryEntry d;

    string full = filename;
    if (full.empty()) return false;

    char head = full.at(0);
    if (!IS_DIRECTORY_SEP(head)) {
	full = path;
	char tail = full.at(full.size() - 1);
	if (!IS_DIRECTORY_SEP(tail)) {
	    full = full.append(DIRECTORY_SEP);
	}
	full = full.append(filename);
    }

    if (stat(full.c_str(), &s) != 0) return false;
    if (!subdirp && S_ISDIR(s.st_mode)) return false;
    if (!unreadablep && (!(s.st_mode & S_IRUSR))) return false;

    d.fullname = full;

    {
	string::size_type fpos;
#ifdef WIN32
	fpos = d.fullname.find_last_of("\\/");
#else
	fpos = d.fullname.rfind('/');
#endif
	if (fpos == string::npos) {
	    d.filename = d.fullname;
	    d.dirname = "";
	} else {
	    d.filename = d.fullname.substr(fpos + 1);
	    d.dirname = d.fullname.substr(0, fpos + 1);
	}
    }

    d.readable_p = (s.st_mode & S_IRUSR);
    d.writable_p = (s.st_mode & S_IWUSR);
    d.directory_p = S_ISDIR(s.st_mode); 
    dirvec.push_back(d);

    return true;
}

bool
DirectoryInfo::refresh(
    const char *x_path
)
{
    if (x_path) path = x_path;
    if (path.empty()) return false;

    if (!dirvec.empty()) dirvec.erase(dirvec.begin(), dirvec.end());

#if defined(WIN32)
#error "TODO"
#elif defined(HAVE_READDIR_R)
    DIR *dir;
    struct dirent *entry;
    struct dirent *result;
    int ret;

    dir = opendir(path.c_str());
    if (!dir) return false;
    entry = (struct dirent*) malloc(sizeof(struct dirent) + NAME_MAX);
    if (!entry) return false;
    while (((ret = readdir_r(dir, entry, &result)) == 0)
	   && result) {
	addfile(result->d_name);
    }
    free(entry);
    closedir(dir);
#else /* normal readdir */
    DIR *dir;
    struct dirent *result;

    dir = opendir(path.c_str());
    if (!dir) return false;
    while (result = readdir(dir)) {
	addfile(result->d_name);
    }
    closedir(dir);
#endif

    return true;
}

DirectoryInfo::DirectoryInfo(
    bool include_subdir_p,
    bool include_unreable_p
)
{
    subdirp = include_subdir_p;
    unreadablep = include_unreable_p;
}

/*******************************************************************************
                             LEDirectoryInfo
*******************************************************************************/

bool
LEDirectoryInfo::addfile(
    const char *filename
)
{
    struct stat s;
    DirectoryEntry d;

    string full = filename;
    if (full.empty()) return false;

    if (stat(full.c_str(), &s) != 0) return false;

    d.fullname = full;

    {
	string::size_type fpos;
#ifdef WIN32
	fpos = d.fullname.find_last_of("\\/");
#else
	fpos = d.fullname.rfind('/');
#endif
	if (fpos == string::npos) {
	    d.filename = d.fullname;
	    d.dirname = "";
	} else {
	    d.filename = d.fullname.substr(fpos + 1);
	    d.dirname = d.fullname.substr(0, fpos + 1);
	}
    }

    d.readable_p = (s.st_mode & S_IRUSR);
    d.writable_p = (s.st_mode & S_IWUSR);
    d.directory_p = S_ISDIR(s.st_mode); 
    dirvec.push_back(d);

    return true;
}

bool
LEDirectoryInfo::refresh(
    const char *x_path
)
{
    DirectoryInfo di;
    DirectoryInfoVec *pdv;

    if (x_path) path = x_path;
    if (path.empty()) return false;

    if (!dirvec.empty()) dirvec.erase(dirvec.begin(), dirvec.end());

    DirectoryInfo::refresh(path.c_str());

    pdv = DirectoryInfo::get_directory_info();
    DirectoryInfoVec::const_iterator i = pdv->begin();
    for (; i != pdv->end();++i) {
	string full = i->dirname;

	if (0 == strcmp("locale", i->filename.c_str())) continue;

	char tail = full.at(full.size() - 1);
	if (!IS_DIRECTORY_SEP(tail)) {
	    full = full.append(DIRECTORY_SEP);
	}

#ifdef WIN32
#define MODEXT ".dll"
#else
#define MODEXT ".so"
#endif
	full = full.append(i->filename);
	if (i->filename.rfind(MODEXT, i->filename.size()) >= i->filename.size()) {
	    full = full.append(DIRECTORY_SEP);
	    full = full.append(i->filename);
	    full = full.append(MODEXT);
	}
	addfile(full.c_str());
    }

    return true;
}

/*******************************************************************************
                             File manipulation.
*******************************************************************************/

CARD8BIT*
IM_read_file(
    const char* filename,
    size_t size
)
{
    size_t rsize;
    FILE *fd;
    CARD8BIT *pr;

    fd = fopen(filename, "rb");
    if (!fd) {
	return NULL;
    }
    pr = (CARD8BIT*) malloc(sizeof(CARD8BIT) * size);
    if (!pr) {
	fclose(fd);
	return NULL;
    }
    rsize = fread(pr, sizeof(CARD8BIT), size, fd);
    fclose(fd);
    if (rsize != size) {
	free(pr);
	return NULL;
    }

    return pr;
}

// return -1 if error occurs.
int IM_file_size(
    const char* filename
)
{
    struct stat s;
    if (stat(filename, &s) != 0) return -1;
    return s.st_size;
}

static int
dir_valid(const char * path_name, uid_t euid) {
    struct stat	st;

#if !defined(S_IAMB)
#define	S_IAMB	0x1FF
#endif /* !S_IAMB */
    if ((0 == lstat(path_name, &st)) &&
	(0 != S_ISDIR(st.st_mode)) &&
	 (euid == st.st_uid) &&
	 ((S_IAMB & st.st_mode) == S_IRWXU)) {
	return 1;
    } else {
	return 0;
    }
}


void
alternate_unix_domain_socket_file_dir(
    string &	user_name,
    uid_t	euid,
    string &	alternate_dir
)
{
    DIR *		dirp;
    struct dirent *	dirent;
    string		base_name;
    string		dir_name;
    char *		dir_str;
    int			length;
    int			found;
    int			fd;
    int			i;
    int			key;

#define ALTERNATE_DIR_RETRY_MAX	(10)

    dirp = opendir("/tmp");
    if (NULL == dirp) return;

    base_name = (string(".iiimp-unix-") + user_name + string("-"));

    found = 0;
    while (NULL != (dirent = readdir(dirp))) {
	if (0 != strncmp(dirent->d_name, base_name.c_str(), base_name.size())) {
	    continue;
	}

	dir_name = (string("/tmp/") + string(dirent->d_name));
	found = dir_valid(dir_name.c_str(), euid);

	if (1 == found) break;
    }

    closedir(dirp);

    if (1 == found) {
	alternate_dir = dir_name;
	return;
    }

    length = strlen("/tmp/.iiimp-unix-") + user_name.size() + 1 + 8 + 1;
    dir_str = (char *)malloc(length);
    if (NULL == dir_str) return;

    fd = open("/dev/urandom", O_RDONLY, 0);
    if (fd <= 0) srandom(time(NULL));

    for (i = 0; i < ALTERNATE_DIR_RETRY_MAX; i++) {
	if (0 <= fd) {
	    read(fd, &key, sizeof (key));
	} else {
	    key = random();
	}

	snprintf(dir_str, length,
		 "/tmp/.iiimp-unix-%s-%08x", user_name.c_str(), key);

	if (mkdir(dir_str, S_IRWXU) < 0) {
	    if (EEXIST != errno) {
		continue;
	    }
	}
	if (1 == dir_valid(dir_str, euid)) {
	    break;
	}
    }

    if (0 <= fd) close(fd);

    if (i < ALTERNATE_DIR_RETRY_MAX) {
	alternate_dir = string(dir_str);
    }
    free(dir_str);

    return;
}

void
IM_unix_domain_socket_file_dir(string user_name, string & dir_ret)
{
    string	dir;
    uid_t	euid;
    struct stat	st;

    euid = geteuid();

    dir = (string("/tmp/.iiimp-unix-") + string(user_name));
    if (0 == dir_valid(dir.c_str(), euid)) {
	alternate_unix_domain_socket_file_dir(user_name, euid, dir_ret);
    } else {
	dir_ret = dir;
    }
}

/*******************************************************************************
			       IMSocket & IMSocketListen
*******************************************************************************/

//
// Address object.
//

void*
IMSocketAddress::get_addrinfo()
{
    struct addrinfo hints, *pst;
    int st;

    if (addrinfo) return addrinfo;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    if (!address.c_str()) hints.ai_flags = AI_PASSIVE;
    st = getaddrinfo(address.c_str(), service.c_str(), &hints, &pst);
    if (st != 0) {
	LOG_ERROR("Fail to getaddrinfo:%s, %s",
		  address.c_str(), service.c_str());
	return NULL;
    }

    addrinfo = pst;

    return pst;
}

void
IMSocketAddress::bind_fail(
    int fd
)
{
    int e = errno;

    LOG_ERROR("Fail to bind socket: %s:%s", address.c_str(), service.c_str());
#ifdef EADDRINUSE
    const int err_addr_in_use = EADDRINUSE;
#else
    const int err_addr_in_use = EINVAL;
#endif
    if (e == err_addr_in_use) {
	LOG_ERROR("Another IIIMP server might be running(address already in use).");
    }

    close(fd);
}

void
IMSocketAddress::configure_socket(
    int fd
)
{
    int reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
}

int
IMSocketAddress::setup_inet(
    void *paddressinfo
)
{
    struct addrinfo *pai = (struct addrinfo*) paddressinfo;

    int fd = socket(pai->ai_family, pai->ai_socktype, pai->ai_protocol);

    if (fd < 0) {
	LOG_ERROR("Cannot open socket for %s:%s",
		  address.c_str(), service.c_str());
	return -1;
    }

    configure_socket(fd);

    if (bind(fd, pai->ai_addr, pai->ai_addrlen) < 0) {
	bind_fail(fd);
	return -1;
    }

    return fd;
}

int
IMSocketAddress::setup_unix()
{

#if defined(HAVE_UNIX_SOCKET)
    struct stat st;
    string srvc;

    srvc = service.c_str();
    if (srvc.size() != 0) {

	memset(&st, 0, sizeof(st));
	if (lstat(address.c_str(), &st) == 0) {
	    if (S_ISDIR(st.st_mode) && (st.st_mode & (S_IXUSR | S_IWUSR))
		&& !S_ISLNK(st.st_mode)) {
	    } else {
		return -1;
	    }
	} else {
	    mode_t mode = (type == UNIX_DOMAIN) ? 0755 : 0700;
	    string dir = address.substr(0, address.find(".iiimp-unix", 0));

	    if (lstat(dir.c_str(), &st) == 0) {
		if (S_ISDIR(st.st_mode) && (st.st_mode & (S_IXUSR | S_IWUSR))
		    && !S_ISLNK(st.st_mode)) {
		    /* just try to make address directory */
		    if (mkdir(address.c_str(), mode) < 0) {
			return -1;
		    }
		} else {
		    return -1;
		}
	    } else {
		/* base directory like /var/run/iiim is missing. */
		if (!mkdir(dir.c_str(), mode)) {
		    if (mkdir(address.c_str(), mode) < 0) {
			return -1;
		    }
		} else {
		    return -1;
		}
	    }
	}
    }

    int fd = socket(AF_UNIX, SOCK_STREAM, 0);

    if (fd == -1) {
	LOG_ERROR("Cannot open socket. (%s)", address.c_str());
	return false;
    }

    configure_socket(fd);

    struct sockaddr_un sun_addr;

    if (srvc.size() != 0) {
	snprintf(sun_addr.sun_path, sizeof(sun_addr.sun_path), "%s/%s",
		 address.c_str(), service.c_str());
    } else {
	strncpy(sun_addr.sun_path, address.c_str(), sizeof(sun_addr.sun_path));
    }
    sun_addr.sun_path[sizeof(sun_addr.sun_path) -1] = '\0';
    sun_addr.sun_family = AF_UNIX;

    size_t size = (offsetof(struct sockaddr_un, sun_path)
	+ strlen (sun_addr.sun_path) + 1);

    if (unlink(sun_addr.sun_path) < 0) {
	if (errno != ENOENT) {
		bind_fail (fd);
		return -1;
	}
    }

    mode_t new_mask = (type == UNIX_DOMAIN) ? 0 : 077;
    mode_t old_mask = umask(new_mask);
    if (bind(fd, (struct sockaddr*) &sun_addr, size) < 0) {
	bind_fail(fd);
	unlink(sun_addr.sun_path);
	return -1;
    }
    umask(old_mask);

    return fd;
#else
    return -1;
#endif
}

int
IMSocketAddress::create_sockets(
    IMSocketVec &sockvec
)
{
    if (type == INET) {
	int i = 0;
	int fd;
	struct addrinfo *pai = (struct addrinfo*) get_addrinfo();
	IMSocket *ps;

	for (; pai; pai = pai->ai_next) {
	    fd = setup_inet(pai);
	    if (fd < 0) continue;
	    ps = new IMSocket(fd, trans_type);
	    // it should be dealt with memory exception.
	    if (!ps) {
		close(fd);
		return -1;
	    }
	    sockvec.push_back(ps);
	    i++;
	}

	if (i == 0) {
	    LOG_CRITICAL("Failed to create any sockets for %s:%s ... exit.",
			 address.c_str(), service.c_str());
	    exit(255);
	}

	return i;
    } else if (type == UNIX_DOMAIN || type == UNIX_DOMAIN_PER_USER) {
	IMSocket *ps;
	int fd = setup_unix();

	if (fd >= 0) {
	    ps = new IMSocket(fd, trans_type);
	    if (!ps) {
		close(fd);
		return -1;
	    }
	}
	if (fd < 0) {
	    LOG_CRITICAL("Failed to create a unix domain socket for %s:%s ... exit.",
			 address.c_str(), service.c_str());
	    exit(255);
	}

	sockvec.push_back(ps);
	return 1;
    }

    ERROR_INTERNAL("invalid IMSocketAddress type");

    return 0;
}

IMSocketAddress::~IMSocketAddress()
{
    if (addrinfo) {
	freeaddrinfo((struct addrinfo*) addrinfo);
    }
#if defined(HAVE_UNIX_SOCKET)
    if (type == UNIX_DOMAIN || type == UNIX_DOMAIN_PER_USER) {
        sockaddr_un sun_addr;
	string dir = address.substr(0, address.find(".iiimp-unix", 0));

        snprintf(sun_addr.sun_path, sizeof(sun_addr.sun_path), "%s/%s",
	     address.c_str(), service.c_str());
        sun_addr.sun_path[sizeof(sun_addr.sun_path) -1] = '\0';
        unlink (sun_addr.sun_path);
	if (is_dir_created || dir != address) {
	    /* try to remove a socket directory which was made by iiimd itself
	       or was expected for IIIMF */
	    rmdir(address.c_str());
	}
    }
#endif
}

IMSocketAddress::
IMSocketAddress(
    ADDRESS_TYPE x_type,
    const string &x_address,
    const string &x_service
) : trans_type(NORMAL), type(x_type), address(x_address), service(x_service)
{
    addrinfo = NULL;
    is_dir_created = false;
}

IMSocketAddress::
IMSocketAddress(
    ADDRESS_TYPE x_type,
    TRANSPORT_TYPE x_trans,
    const string &x_address,
    const string &x_service
) : trans_type(x_trans), type(x_type), address(x_address), service(x_service)
{
    addrinfo = NULL;
    is_dir_created = false;
}

//
// Socket object.
//

bool
IMSocket::do_listen()
{
    if (::listen(fd, 5) < 0) {
	LOG_ERROR("Fail to listen:%d", fd);
	close(fd);
	status = ERROR;
	return false;
    }

    status = OPEN;

    return true;
}

bool
IMSocket::listen()
{
    if (status == ERROR) return false;
    if (status == OPEN) return true;

    return do_listen();
}

IMSocketTrans*
IMSocket::accept()
{
    int st;
#ifdef HAVE_SOCKADDR_STORAGE
    struct sockaddr_storage ss;
#else
    struct sockaddr ss;
#endif
    struct sockaddr *psa = (struct sockaddr*) &ss;
    socklen_t fromlen;

    if (status == ERROR) return NULL;

    do {
	fromlen = sizeof(ss);
	st = ::accept(fd, psa, &fromlen);
	if (st != -1) {
#ifdef HAVE_TLS
            return IMTLS::get_instance()->create_trans(st, trans_type);
#else
	    return new IMSocketTrans(st);
#endif
	}
    } while (errno == EINTR);

    status = ERROR;
    LOG_ERROR("Could not accept.\n");

    return NULL;
}

int
IMSocket::get_fd()
{
    if (status == OPEN) return fd;
    return -1;
}

IMSocket::IMSocket(
    int x_fd
) : fd(x_fd), status(CLOSED), trans_type(IMSocketAddress::NORMAL)
{
}

IMSocket::~IMSocket()
{
    if (status == OPEN) {
	close(fd);
    }
}

IMSocket::IMSocket(
    int x_fd,
    int x_trans
) : fd(x_fd), status(CLOSED), trans_type(x_trans)
{
}

//
// Trans object.
//

int
IMSocketTrans::send(
    const void *p,
    size_t n
)
{
    return ::send(fd, p, n, 0);
}

int
IMSocketTrans::recv(
    void *p,
    size_t n
)
{
    return ::recv(fd, p, n, 0);
}

IMSocketTrans::IMSocketTrans(
    int x_fd
)
{
    fd = x_fd;
}

IMSocketTrans::~IMSocketTrans()
{
    if (fd > 0) {
	shutdown(fd, SHUT_RDWR);
	close(fd);
    }
}


//
// Listen object.
//

bool
IMSocketListen::listen(
    IMSocketAddressVec& endvec
)
{
    if (status == IMSocket::ERROR) return false;
    if (status == IMSocket::OPEN) return true;

    IMSocketAddressVec::iterator ait;
    for (ait = endvec.begin(); ait!= endvec.end(); ait++) {
	if (ait->create_sockets(sockvec) < 0) return false;
    }

    IMSocketVec::iterator it;
    IMSocket *pims;
    for (it = sockvec.begin(); it != sockvec.end(); it++) {
	pims = *it;
	if (pims->get_status() == IMSocket::ERROR) return false;
	if (pims->get_status() == IMSocket::OPEN) continue;
	if (!pims->listen()) {
	    status = IMSocket::ERROR;
	    return false;
	}
    }
    status = IMSocket::OPEN;

    return true;
}

IMSocketTrans*
IMSocketListen::accept()
{
    IMSocketVec::iterator it;
    IMSocket *pims;

    if (status != IMSocket::OPEN) return NULL;

#if defined(HAVE_POLL)
    int fd, i, n;
    struct pollfd *fds;

    i = sockvec.size();
    if (i <= 0) return NULL;
    fds = new struct pollfd[i];
    if (!fds) return NULL;
    for (n = 0, it = sockvec.begin(); it != sockvec.end(); it++) {
	pims = *it;
	fd = pims->get_fd();
	if (fd < 0) continue;
	fds[n].fd = fd;
	fds[n].events = POLLIN;
	fds[n].revents = 0;
	n++;
    }
    if (n > 0) {
	i = poll(fds, n, -1);
	if (i > 0) {
	    for (i = 0; i < n; i++) {
		if (fds[i].revents) {
		    fd = fds[i].fd;
		    for (it = sockvec.begin(); it != sockvec.end(); it++) {
			pims = *it;
			if (fd == pims->get_fd()) {
			    delete [] fds;
			    return pims->accept();
			}
		    }
		}
	    }
	}
    }
    delete [] fds;

#elif defined(HAVE_SELECT)
    int fd, maxfd;
    fd_set fds;
    FD_ZERO(&fds);

    maxfd = -1;
    for (it = sockvec.begin(); it != sockvec.end(); it++) {
	pims = *it;
	fd = pims->get_fd();
	if (fd < 0) continue;
	if (fd > maxfd) maxfd = fd;
	FD_SET(fd, &fds);
    }
    if (maxfd < 0) return NULL;
    maxfd++;
    fd = select(maxfd, &fds, NULL, NULL, NULL);
    if (fd <= 0) return NULL;
    for (it = sockvec.begin(); it != sockvec.end(); it++) {
	pims = *it;
	fd = pims->get_fd();
	if (fd < 0) continue;
	if (FD_ISSET(fd, &fds)) {
	    fd = pims->accept();
	    if (fd < 0) return NULL;
	    return fd;
	}
    }
#else
#error "We need poll() or select() to multiplex socket handles"
#endif

    return NULL;
}

IMSocketListen::IMSocketListen()
{
    status = IMSocket::CLOSED;
}

IMSocketListen::~IMSocketListen()
{
    delete_all(sockvec);
}

/*******************************************************************************
                    IMDaemon (helper object to make a program a daemon)
*******************************************************************************/

IMDaemon* IMDaemon::pimdaemon = NULL;

IMDaemon::IMDaemon()
{
    already_finished = false;
}

IMDaemon*
IMDaemon::get_instance()
{
    if (pimdaemon) return pimdaemon;
    pimdaemon = new IMDaemon();
    return pimdaemon;
}

void
IMDaemon::cleanup()
{
    if (pimdaemon) delete pimdaemon;
}

void
IMDaemon::closefd()
{
    int fd;
#ifdef HAVE_SYSCONF
    int fdmax = sysconf(_SC_OPEN_MAX);
#else
    int fdmax = 3;
#endif
    for (fd = 0; fd < fdmax; fd++) {
	close(fd);
    }

#ifndef WIN32
    // 0 : discard stdout
    open("/dev/null", O_RDWR);
    // 1 : discard stdin
    dup(0);
    // 2 : discard stderr
    dup(0);
#endif
}

bool
IMDaemon::discard_parent()
{
#ifdef HAVE_FORK
    pid_t pid = fork();
    if (pid != 0) {
	if (pid == -1) {
	    return false;
	} else {
	    _exit(0);
	}
    }
#endif
    return true;
}

bool
IMDaemon::daemonize()
{
    if (!discard_parent()) return false;
#ifdef HAVE_SETSID
    if (setsid() < 0)
	return false;
#endif
    if (!discard_parent()) return false;

    return true;
}

void
IMDaemon::setup(
    const char* todir
)
{
    if (!already_finished) {
	if (!daemonize()) {
	    LOG_ERROR("Fail to make a daemon.");
	}
	closefd();
    }
    if (todir) {
	chdir(todir);
    }
}

/* Local Variables: */
/* c-file-style: "iiim-project" */
/* End: */
