/*
 * Copyright (C) 2006 iptelorg GmbH
 *
 * This file is part of Kamailio, a free SIP server.
 *
 * Kamailio is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version
 *
 * Kamailio is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "init_socks.h"
#include "../../core/dprint.h"
#include "../../core/ip_addr.h"
#include "../../core/resolve.h"

#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h> /*IPTOS_LOWDELAY*/
#include <netinet/tcp.h>
#include <sys/un.h>
#include <unistd.h>	  /* unlink */
#include <sys/stat.h> /* chmod */
#include <fcntl.h>

#ifndef UNIX_PATH_MAX
#define UNIX_PATH_MAX 104
#endif


static int tcp_proto_no = -1; /* tcp protocol number as returned by
							   getprotobyname */


/* returns -1 on error */
static int set_non_blocking(int s)
{
	int flags;
	/* non-blocking */
	flags = fcntl(s, F_GETFL);
	if(flags == -1) {
		LOG(L_ERR, "ERROR: set_non_blocking: fnctl failed: (%d) %s\n", errno,
				strerror(errno));
		goto error;
	}
	if(fcntl(s, F_SETFL, flags | O_NONBLOCK) == -1) {
		LOG(L_ERR,
				"ERROR: set_non_blocking: fcntl: set non-blocking failed:"
				" (%d) %s\n",
				errno, strerror(errno));
		goto error;
	}
	return 0;
error:
	return -1;
}


/* opens, binds and listens-on a control unix socket of type 'type'
 * it will change the permissions to perm, if perm!=0
 * and the ownership to uid.gid if !=-1
 * returns socket fd or -1 on error */
int init_unix_sock(struct sockaddr_un *su, char *name, int type, int perm,
		int uid, int gid)
{
	struct sockaddr_un ifsun;
	int s;
	int len;
	int optval;

	s = -1;
	unlink(name);
	memset(&ifsun, 0, sizeof(struct sockaddr_un));
	len = strlen(name);
	if(len > UNIX_PATH_MAX) {
		LOG(L_ERR, "ERROR: init_unix_sock: name too long (%d > %d): %s\n", len,
				UNIX_PATH_MAX, name);
		goto error;
	}
	ifsun.sun_family = AF_UNIX;
	memcpy(ifsun.sun_path, name, len);
#ifdef HAVE_SOCKADDR_SA_LEN
	ifsun.sun_len = len;
#endif
	s = socket(PF_UNIX, type, 0);
	if(s == -1) {
		LOG(L_ERR,
				"ERROR: init_unix_sock: cannot create unix socket %s:"
				" %s [%d]\n",
				name, strerror(errno), errno);
		goto error;
	}
	optval = 1;
	if(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
		LOG(L_ERR, "ERROR: init_unix_sock: setsockopt: %s [%d]\n",
				strerror(errno), errno);
		/* continue */
	}
	if(set_non_blocking(s) == -1) {
		LOG(L_ERR, "ERROR: init_unix_sock: set non blocking failed\n");
	}
	if(bind(s, (struct sockaddr *)&ifsun, sizeof(ifsun)) == -1) {
		LOG(L_ERR, "ERROR: init_unix_sock: bind: %s [%d]\n", strerror(errno),
				errno);
		goto error;
	}
	/* then the permissions */
	if(perm) { /* mode==0 doesn't make sense, nobody can read/write */
		if(chmod(name, perm) < 0) {
			LOG(L_ERR,
					"ERROR: init_unix_sock: failed to change the"
					" permissions for %s to %04o: %s[%d]\n",
					name, perm, strerror(errno), errno);
			goto error;
		}
	}
	/* try to change ownership */
	if((uid != -1) && (gid != -1)) {
		if(chown(name, uid, gid) < 0) {
			LOG(L_ERR,
					"ERROR: init_unix_sock: failed to change the"
					" owner/group for %s to %d.%d: %s[%d]\n",
					name, uid, gid, strerror(errno), errno);
			goto error;
		}
	}
	if((type == SOCK_STREAM) && (listen(s, 128) == -1)) {
		LOG(L_ERR, "ERROR: init_unix_sock: listen: %s [%d]\n", strerror(errno),
				errno);
		goto error;
	}
	*su = ifsun;
	return s;
error:
	if(s != -1)
		close(s);
	return -1;
}


/* opens, binds and listens-on a control tcp socket
 * returns socket fd or -1 on error */
int init_tcpudp_sock(union sockaddr_union *sa_un, char *address, int port,
		enum socket_protos type)
{
	union sockaddr_union su;
	struct hostent *he;
	int s;
	int optval;

	s = -1;
	if((type != UDP_SOCK) && (type != TCP_SOCK)) {
		LOG(L_CRIT, "BUG: init_tcpudp_sock called with bad type: %d\n", type);
		goto error;
	}
	memset(&su, 0, sizeof(su));
	/* if no address specified, or address=='*', listen on all
	 * ipv4 addresses */
	if((address == 0) || ((*address) == 0)
			|| ((*address == '*') && (*(address + 1) == 0))) {
		su.sin.sin_family = AF_INET;
		su.sin.sin_port = htons(port);
		su.sin.sin_addr.s_addr = INADDR_ANY;
#ifdef HAVE_SOCKADDR_SA_LEN
		su.sin.sin_len = sizeof(struct sockaddr_in);
#endif
	} else {
		he = resolvehost(address);
		if(he == 0) {
			LOG(L_ERR, "ERROR: init_tcpudp_sock: bad address %s\n", address);
			goto error;
		}
		if(hostent2su(&su, he, 0, port) == -1)
			goto error;
	}
	s = socket(AF2PF(su.s.sa_family),
			(type == TCP_SOCK) ? SOCK_STREAM : SOCK_DGRAM, 0);
	if(s == -1) {
		LOG(L_ERR,
				"ERROR: init_tcpudp_sock: cannot create tcp socket:"
				" %s [%d]\n",
				strerror(errno), errno);
		goto error;
	}
	/* REUSEADDR */
	optval = 1;
	if(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
		LOG(L_ERR, "ERROR: init_tcpudp_sock: setsockopt: %s [%d]\n",
				strerror(errno), errno);
		/* continue */
	}
	/* tos */
	optval = IPTOS_LOWDELAY;
	if(setsockopt(s, IPPROTO_IP, IP_TOS, (void *)&optval, sizeof(optval))
			== -1) {
		LOG(L_WARN, "WARNING: init_tcpudp_sock: setsockopt tos: %s\n",
				strerror(errno));
		/* continue since this is not critical */
	}
	if(set_non_blocking(s) == -1) {
		LOG(L_ERR, "ERROR: init_tcpudp_sock: set non blocking failed\n");
	}

	if(bind(s, &su.s, sockaddru_len(su)) == -1) {
		LOG(L_ERR, "ERROR: init_tcpudp_sock: bind: %s [%d]\n", strerror(errno),
				errno);
		goto error;
	}
	if((type == TCP_SOCK) && (listen(s, 128) == -1)) {
		LOG(L_ERR, "ERROR: init_tcpudp_sock: listen: %s [%d]\n",
				strerror(errno), errno);
		goto error;
	}
	*sa_un = su;
	return s;
error:
	if(s != -1)
		close(s);
	return -1;
}


/* set all socket/fd options:  disable nagle, tos lowdelay, non-blocking
 * return -1 on error */
int init_sock_opt(int s, enum socket_protos type)
{
	int optval;
#ifdef DISABLE_NAGLE
	int flags;
	struct protoent *pe;
#endif

	if((type == UDP_SOCK) || (type == TCP_SOCK)) {
#ifdef DISABLE_NAGLE
		if(type == TCP_SOCK) {
			flags = 1;
			if(tcp_proto_no == -1) { /* if not already set */
				pe = getprotobyname("tcp");
				if(pe != 0) {
					tcp_proto_no = pe->p_proto;
				}
			}
			if((tcp_proto_no != -1)
					&& (setsockopt(s, tcp_proto_no, TCP_NODELAY, &flags,
								sizeof(flags))
							< 0)) {
				LOG(L_WARN,
						"WARNING: init_sock_opt: could not disable"
						" Nagle: %s\n",
						strerror(errno));
			}
		}
#endif
		/* tos*/
		optval = IPTOS_LOWDELAY;
		if(setsockopt(s, IPPROTO_IP, IP_TOS, (void *)&optval, sizeof(optval))
				== -1) {
			LOG(L_WARN, "WARNING: init_sock_opt: setsockopt tos: %s\n",
					strerror(errno));
			/* continue since this is not critical */
		}
	}
	if(set_non_blocking(s) == -1) {
		LOG(L_ERR, "ERROR: init_sock_opt: set non blocking failed\n");
	}
	return 0;
}
