/*
 * Copyright (c) 2004-2009, Luiz Otavio O Souza <loos.br@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

static const char rcsid[] = "$Id: net-io.c 112 2009-03-15 17:30:28Z loos-br $";

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>

#include <netinet/in.h>

#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>

#include "fmt.h"
#include "return.h"
#include "msn-proxy.h"

int
set_options(int fd) {
 struct linger		linger;
 int			keepalive = 1;

    /* set keepalive */
    if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char *)&keepalive,
						sizeof(keepalive)) == -1)
	return(-1);

    /* turn off linger */
    memset(&linger, 0, sizeof(linger));
    if (setsockopt(fd, SOL_SOCKET, SO_LINGER, (char *)&linger,
						sizeof(linger)) == -1)
	return(-1);

    return(0);
}

int
set_nonblock(int fd) {
    return(fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK));
}

int
unix_listen(int backlog) {
 struct sockaddr_un	sun;
 log_			*log = &config.log;
 int			s;

    (void)unlink(MSNPROXYCTL);
    memset(&sun, 0, sizeof(sun));
    sun.sun_family = AF_UNIX;
    (void)strncpy(sun.sun_path, MSNPROXYCTL, sizeof(sun.sun_path));
    s = socket(AF_UNIX, SOCK_STREAM, 0);
    if (s < 0 ||
	bind(s, (struct sockaddr *)&sun, SUN_LEN(&sun)) < 0 ||
	listen(s, backlog) < 0 ||
	chmod(MSNPROXYCTL, 0666) < 0) {

	log->debug("cannot create socket [%S][%S]\n", MSNPROXYCTL,
		   strerror(errno));
	if (s > 0) (void)close(s);
	s = -1;
    }
    return(s);
}

int
net_listen(const char *host, const char *port, int backlog) {
 struct	addrinfo	hints, *res;
 log_			*log = &config.log;
 int			listenfd;
 int			err;

    /* make hints */
    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = PF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    /* make query */
    err = getaddrinfo(host, port, &hints, &res);
    if (err) {
	log->debug("unable to get local address [%S:%S]: %S\n", host, port,
							gai_strerror(err));
	return(-1);
    }

    /* create socket */
    err = 1;
    if ((listenfd = socket(res->ai_family, res->ai_socktype,
						   res->ai_protocol)) == -1 ||
      setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,     &err, sizeof(err)) ||
      bind(listenfd, (struct sockaddr *)res->ai_addr,
						(socklen_t)res->ai_addrlen) ||
      listen(listenfd, backlog)) {

	if (listenfd > 0)
	    close(listenfd);
	freeaddrinfo(res);
	return(-1);
    }
    freeaddrinfo(res);

    log->info("listen on [%S:%S]\n", host, port);

    return(listenfd);
}

int
net_connect(const char *host, const char *port) {
 struct addrinfo	hints, *res, *res0;
 log_			*log = &config.log;
 int			server_fd = -1;
 int			err;

    /* make hints */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    /* get address info */
    err = getaddrinfo(host, port, &hints, &res0);
    if (err) {
	log->debug("Unable to connect [%S:%S]: %S\n", host, port,
						      gai_strerror(err));
	return(-1);
    }

    for (res = res0; res; res = res->ai_next) {

	/* create socket */
	if ((server_fd = socket(res->ai_family, res->ai_socktype,
						res->ai_protocol)) == -1) {

	    log->debug("unable to create server socket: %S\n", strerror(errno));
	    freeaddrinfo(res0);
	    return(-1);
	}

	/* set socket options */
	if (set_options(server_fd) == -1 || set_nonblock(server_fd) == -1) {
	    while (close(server_fd) != 0 && errno == EINTR);
	    freeaddrinfo(res0);
	    return(-1);
	}

	/* connect */
	for (;;) {
	    if (connect(server_fd, res->ai_addr, res->ai_addrlen) == -1) {
		if (errno == EINPROGRESS)
		    break;

		if (errno == EINTR)
		    continue;

		log->debug("unable to connect to server [%S:%S]: %S\n",
						host, port, strerror(errno));
		while (close(server_fd) != 0 && errno == EINTR);
		server_fd = -1;
	    }
	    break;
	}
	if (server_fd == -1) continue;

	freeaddrinfo(res0);
	log->info("connected to [%S:%S]\n", host, port);
	return(server_fd);
    }

    log->info("unable to connect to server [%S:%S]: %S\n", host, port,
	      strerror(errno));
    freeaddrinfo(res0);
    return(-1);
}

string *bind_free_port(
			unsigned int port_min,
			unsigned int port_max,
			string *host,
			int *listenfd) {
 log_			*log = &config.log;
 string			*port;
 unsigned int		count;
 unsigned int		random_port;

    /* default */
    *listenfd = -1;

    /* verify ports */
    if (port_min < 0 || port_min > port_max || port_max > 65535) {
	log->debug("debug: invalid port range (check config)\n");
	return((string *)0);
    }

    /* alloc new string */
    port = str_alloc();
    if (port == (string *)0)
	die_nomem();

    /* find unused port in range */
    for (count = 0; count < 10; count++) {

	for (;;) {
	    random_port = (unsigned int) random() % 65535;
	    if (random_port < port_min || random_port > port_max) {
		usleep(random_port % getpid());
		continue;
	    }
	    break;
	}

	/* fmt port */
	port->len = fmt_printf(NULL, "%d", random_port);
	if (str_ready(port, port->len + 1) == 0)
	    die_nomem();
	port->len = fmt_printf(port->s, "%d", random_port);

	*listenfd = net_listen((char *)host->s, (char *)port->s, 1);
	if (*listenfd == -1)
	    continue;

	break;
    }

    /* no more ports in range */
    if (*listenfd == -1) {
	log->debug("debug: cannot find free port to bind. check the internal ip config.\n");
	str_free(port);
	free(port);
	return((string *)0);
    }

    /* fmt return [IP:PORT] */
    port->len = fmt_printf(NULL, "%S:%d", host->s, random_port);
    if (str_ready(port, port->len + 1) == 0)
	die_nomem();
    port->len = fmt_printf(port->s, "%S:%d", host->s, random_port);

    return(port);
}

int
resolve_client(string *str_host, struct sockaddr *sa, socklen_t sa_len) {
 static char		host[NI_MAXHOST];

    memset(host, 0, NI_MAXHOST);

    /* get the user network address */
    if (getnameinfo(sa, sa_len, host, sizeof(host), NULL, 0, NI_NUMERICHOST) != 0)
        return(RFAIL);

    if (str_copys(str_host, (unsigned char *)host) == 0)
	die_nomem();

    return(ROK);
}
