/*
 * -------------------------------------------------------
 * Copyright (C) 2002-2007 Tommi Saviranta <wnd@iki.fi>
 *	(C) 2002 Lee Hardy <lee@leeh.co.uk>
 *	(C) 1998-2002 Sebastian Kienzl <zap@riot.org>
 * -------------------------------------------------------
 * This program 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.
 *
 * This program 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.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* ifdef HAVE_CONFIG_H */

#include "irc.h"
#include "common.h"
#include "conntype.h"
#include "llist.h"
#include "perm.h"

#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#if HAVE_STRINGS_H
#include <strings.h>
#endif



/* #define DEBUG */

#ifdef HAVE_HSTRERROR
#ifndef HAVE_HSTRERROR_PROTO
const char *hstrerror(int err);
#endif /* ifndef HAVE_HSTRERROR_PROTO */
#endif /* ifdef HAVE_HSTRERROR */

/* va_copy */
#ifndef va_copy
#ifdef HAVE___VA_COPY
#define va_copy __va_copy
#else /* ifdef HAVE___VA_COPY */
/* we may need "memcpy(&dest, &src, sizeof(va_list))" on some systems */
#define va_copy(dest, src) ((dest) = (src))
#endif /* ifdef else HAVE___VA_COPY */
#endif /* ifndef va_copy */


#define HEAD	1
#define TAIL	2

static void track_highest(void);
static void track_add(int s);
static void track_del(int s);
static int irc_write_smart(connection_type *connection, int queue,
		char *format, va_list va);

struct hostent *hostinfo = NULL;
#ifdef IPV6
struct sockaddr_in6 addr;
#else
struct sockaddr_in addr;
#endif

const char *net_errstr;

#define TRACK 512
int track_socks[TRACK];

int highest_socket = 0;

int		msgtimer;	/* Message timer... */
llist_list	msg_queue;	/* Message queue */



static void
track_highest(void)
{
	int	i;
	highest_socket = 0;
	for (i = 0; i < TRACK; i++) {
		if (track_socks[i] > highest_socket) {
			highest_socket = track_socks[i];
		}
	}
} /* void track_highest(void) */



static void
track_add(int s)
{
	int	i = 0;
	while (track_socks[i] && i < TRACK) {
		i++;
	}
	if (i < TRACK) {
		track_socks[i] = s;
	}
	track_highest();
} /* void track_add(int s) */



static void
track_del(int s)
{
	int	i;
	for (i = 0; i < TRACK; i++) {
		if (track_socks[i] == s) {
			track_socks[i] = 0;
		}
	}
	track_highest();
} /* void track_del(int s) */



/*
 * Creates a socket.
 *
 * Returns number of opened socket or -1 if something went wrong.
 */
int
sock_open(void)
{
	int	i;

#ifdef IPV6
	if ((i = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)) < 0)
#else
	if ((i = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
#endif
	{
		net_errstr = strerror(errno);
		return -1;
	}
	track_add(i);
	sock_setreuse(i);
	/* local reuse by default */
	return i;
} /* int sock_open(void) */



int
rawsock_close(int sock)
{
	if (! sock) return 1;

	track_del(sock);
	close(sock);
	return 1;
} /* int rawsock_close(int sock) */



int
sock_close(connection_type *connection)
{
	rawsock_close(connection->socket);
	connection->socket = 0;
	return 1;
} /* int sock_close(connection_type *connection) */



int
sock_setnonblock(int sock)
{
	if (fcntl(sock, F_SETFL, O_NONBLOCK) < 0) {
		net_errstr = strerror(errno);
		return 0;
	} else {
		return 1;
	}
} /* int sock_setnonblock(int sock) */



int
sock_setblock(int sock)
{
	int	flags;

	flags = fcntl(sock, F_GETFL, 0);
	if (flags == -1) {
		net_errstr = strerror(errno);
		return 0;
	}

	flags &= ~O_NONBLOCK;

	if (fcntl(sock, F_SETFL, flags) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}

	return 1;
} /* int sock_setblock(int sock) */



int
sock_setreuse(int sock)
{
	int	i = 1;
	
	if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void *) &i,
				sizeof(i)) < 0) {
		net_errstr = strerror(errno);
		return 0;
	} else {
		return 1;
	}
} /* int sock_setreuse(int sock) */



struct hostent *
name_lookup(char *host)
{
#ifdef IPV6
	hostinfo = gethostbyname2(host, AF_INET6);
	if (hostinfo != NULL) {
		return hostinfo;
	}
#else /* IPV6 */
	hostinfo = gethostbyname(host);
	if (hostinfo != NULL) {
		return hostinfo;
	}
#endif /* IPV6 */

	/* TODO: hstrerror is obsolete */
#ifdef HAVE_HSTRERROR
	net_errstr = (const char *) hstrerror(h_errno);
#else
	net_errstr = "unable to resolve";
#endif
	return NULL;
} /* struct hostent *name_lookup(char *host) */



#ifdef IPV6
int
sock_bind(int sock, char *bindhost, int port)
{
	/* We'd better cast &addr to void * to keep Digital-UNIX happy. */
	memset((void *) &addr, 0, sizeof(struct sockaddr_in6));
	
	addr.sin6_addr = in6addr_any;
	addr.sin6_family = AF_INET6;
	
	if (bindhost) {
		if (! name_lookup(bindhost)) {
			return 0;
		}

		memcpy((char *) &addr.sin6_addr, hostinfo->h_addr,
				hostinfo->h_length);
		addr.sin6_family = hostinfo->h_addrtype;
	}

	addr.sin6_port = htons((u_short)port);

	if (bind(sock, (struct sockaddr *) &addr,
				sizeof(struct sockaddr_in6)) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}

	return 1;
} /* int sock_bind(int sock, char *bindhost, int port) */



int
sock_bindlookedup(int sock, int port)
{
	/* We'd better cast &addr to void * to keep Digital-UNIX happy. */
	memset((void *) &addr, 0, sizeof(struct sockaddr_in6));

	addr.sin6_addr = in6addr_any;
	addr.sin6_family = AF_INET6;
	
	if (hostinfo) {
		memcpy((char *) &addr.sin6_addr, hostinfo->h_addr,
				hostinfo->h_length);
		addr.sin6_family = hostinfo->h_addrtype;
	}

	addr.sin6_port = htons((u_short)port);
	
	if (bind(sock, (struct sockaddr *)&addr,
				sizeof(struct sockaddr_in6)) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}

	return 1;
} /* int sock_bindlookedup(int sock, int port) */

#else

int
sock_bind(int sock, char *bindhost, int port)
{
	/* We'd better cast &addr to void * to keep Digital-UNIX happy. */
	memset((void *) &addr, 0, sizeof(struct sockaddr_in));

	addr.sin_addr.s_addr = INADDR_ANY;
	addr.sin_family = AF_INET;
	
	if (bindhost) {
		if (name_lookup(bindhost) == NULL) {
			return 0;
		}
		
		memcpy((char *) &addr.sin_addr, hostinfo->h_addr,
				hostinfo->h_length);
		addr.sin_family = hostinfo->h_addrtype;
	}
	
	addr.sin_port = htons((u_short) port);
	
	if (bind(sock, (struct sockaddr *) &addr, 
				sizeof(struct sockaddr)) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}

	return 1;
} /* int sock_bind(int sock, char *bindhost, int port) */



int
sock_bindlookedup(int sock, int port)
{
	/* We'd better cast &addr to void * to keep Digital-UNIX happy. */
	memset((void *) &addr, 0, sizeof(struct sockaddr_in));

	addr.sin_addr.s_addr = INADDR_ANY;
	addr.sin_family = AF_INET;
	
	if (hostinfo) {
		memcpy((char *) &addr.sin_addr, hostinfo->h_addr,
				hostinfo->h_length);
		addr.sin_family = hostinfo->h_addrtype;
	}

	addr.sin_port = htons((u_short) port);
	
	if (bind(sock, (struct sockaddr *) &addr,
				sizeof(struct sockaddr)) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}
	
	return 1;
} /* int sock_bindlookedup(int sock, int port) */

#endif



int
sock_listen(int sock)
{
	if (! sock_setnonblock(sock)) {
		return 0;
	}

	if (listen(sock, QUEUESIZE) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}
	
	return 1;
} /* int sock_listen(int sock) */



/*
 * Accept socket. Return socket.
 *
 * If checkperm is non-zero, a check is made to be sure connecting IP is allowed
 * to establish a connection with us. If connection is unauthorized, connection
 * will be closed and function returns -1.
 */
int
sock_accept(int sock, char **s, int checkperm)
{
	socklen_t	temp;
	int		store;
#ifdef IPV6
	char		ip[40];
	char		ipv6[512];
#else
	char		*ip;
#endif
	int		perm;

#ifdef IPV6
	temp = sizeof(struct sockaddr_in6);
#else
	temp = sizeof(struct sockaddr_in);
#endif
	if ((store = accept(sock, (struct sockaddr *) &addr, &temp)) < 0) {
		net_errstr = strerror(errno);
		return 0;
	}

#ifdef IPV6
	inet_ntop(AF_INET6, (char *) &addr.sin6_addr, ip,
			sizeof(addr.sin6_addr));
#else
	ip = inet_ntoa(addr.sin_addr);
#endif
	perm = is_perm(&connhostlist, ip);

#ifdef IPV6
	if (! getnameinfo((struct sockaddr *) &addr, sizeof(addr), ipv6,
				sizeof(ipv6), 0, 0, 0)) {
		*s = xstrdup(ipv6);
		perm |= is_perm(&connhostlist, *s);
	} else {
		*s = xstrdup(ip);
	}

#else
	hostinfo = gethostbyaddr((char *) &addr.sin_addr.s_addr,
			sizeof(struct in_addr), AF_INET);
	
	if (hostinfo) {
		*s = xstrdup(hostinfo->h_name);
		perm |= is_perm(&connhostlist, *s);
	} else {
		*s = xstrdup(ip);
	}
#endif

	if (! checkperm || perm) {
		track_add(store);
		return store;
	} else {
		close(store);
		return -1;
	}
} /* int sock_accept(int sock, char **s, int checkperm) */



/*
 * Send data to all connected clients.
 */
int
irc_mwrite(clientlist_type *clients, char *format, ...)
{
	llist_node *client;
	va_list va;
	int ret;

	if (clients->connected == 0) {
		return 0;
	}

	ret = 0;
	va_start(va, format);
	for (client = clients->clients->head; client != NULL;
			client = client->next) {
		va_list cva;
		/*
		 * Having '"%s", buffer' instead of plain 'buffer' is essential
		 * because we don't want our string processed any further by
		 * va.
		 */
		va_copy(cva, va);
		ret += irc_write_smart((connection_type *) client->data, TAIL,
				format, cva);
		va_end(cva);
	}
	va_end(va);

	return (ret != 0);
} /* int irc_mwrite(clientlist_type *clients, char *format, ...) */



/*
 * Put message in front of queue.
 *
 * We need this to make sure PONGs are not delayed for too long.
 */
int
irc_write_head(connection_type *connection, char *format, ...)
{
	va_list va;
	int r;

	va_start(va, format);
	r = irc_write_smart(connection, HEAD, format, va);
	va_end(va);

	return r;
} /* int irc_write_head(connection_type *connection, char *format, ...) */



/* must NOT be called internally -- call irc_write_smart instead */
int
irc_write(connection_type *connection, char *format, ...)
{
	va_list va;
	int r;

	va_start(va, format);
	r = irc_write_smart(connection, TAIL, format, va);
	va_end(va);

	return r;
} /* int irc_write(connection_type *connection, char *format, ...) */


/*
 * Send messages "the smart way".
 *
 * If message is being sent to client, send it instantly. If message is going
 * to the server and flood control allows this, send message and decrease flood
 * counter. Otherwise put message in queue.
 */
static int
irc_write_smart(connection_type *connection, int queue,
		char *format, va_list va)
{
	char buffer[IRC_MSGLEN];

	vsnprintf(buffer, IRC_MSGLEN - 2, format, va);
	buffer[IRC_MSGLEN - 3] = '\0';
	strcat(buffer, "\r\n");

	if (connection != &c_server) {
		return irc_write_real(connection, buffer);
	} else if (msgtimer > 1) {
		msgtimer--;
		return irc_write_real(connection, buffer);
	} else {
		/*
		 * All messages in queue are due to sending to server so
		 * there is no need to save target of these messages.
		 */
		if (queue == TAIL) {
			llist_add_tail(llist_create(xstrdup(buffer)),
					&msg_queue);
		} else {
			llist_add(llist_create(xstrdup(buffer)),
					&msg_queue);
		}
		return strlen(buffer);
	}
} /* static int irc_write_smart(connection_type *connection, int queue,
		char *foramt, va_list va) */



int
irc_write_real(connection_type *connection, char *buffer)
{
#ifdef DEBUG
	fprintf(stdout, ">>%03d>> %s", connection->socket, buffer);
	fflush(stdout);
#endif
	return send(connection->socket, buffer, strlen(buffer), 0);
} /* int irc_write_real(connection_type *connection, char *buffer) */



/*
 * Process send queue.
 *
 * miau can throttle messages (to server) so that we don't get disconnected
 * from the server for excess flood. As long as queue is non-empty and flood
 * control allows us to send, send first message in queue and decrese flood
 * control counter.
 */
void
irc_process_queue(void)
{
	char *buf;
	/*
	 * Basically there can be only one message in queue that can be
	 * sent "per round" but there are exeptions. If executing miau is
	 * delayed because of heavy system load or suspended miau, next
	 * time timers are checked, msgtimer is increased (possibly by
	 * more than one) like there was no delay (in execution of miau).
	 *
	 * We could, of course, increase counter by one no matter what
	 * happened, but seriously, the code would be only _very_ little
	 * smaller... And this "the way to go", don't you think ?-)
	 */
	while (msg_queue.head != NULL && msgtimer > 0) {
		buf = (char *) msg_queue.head->data;
		irc_write_real(&c_server, buf);
		/* If message is 'MODE' message, add additional penalty. */
		if (strlen(buf) >= 4 && buf[0] == 'M' && buf[1] == 'O'
				&& buf[2] == 'D' && buf[3] == 'E') {
			msgtimer--;
		}
		/* After message is sent, remove it from queue. */
		xfree(msg_queue.head->data);
		llist_delete(msg_queue.head, &msg_queue);
		/* Finally decrease flood control counter. */
		msgtimer--;
	}
} /* void irc_process_queue(void) */



/*
 * Clear send queue.
 *
 * Removes all lines from send queue. This function is most likely called
 * when miau disconnects from the server or when miau is being shut down.
 */
void
irc_clear_queue(void)
{
	while (msg_queue.head != NULL) {
		xfree(msg_queue.head->data);
		llist_delete(msg_queue.head, &msg_queue);
	}
} /* void irc_clear_queue(void) */



static int
irc_notice_va(connection_type *conn, const char *nick, const char *format,
		va_list va)
{
	char buf[IRC_MSGLEN];
	int r;

	vsnprintf(buf, IRC_MSGLEN - 9, format, va);
	buf[IRC_MSGLEN - 10] = '\0';
	r = irc_write(conn, "NOTICE %s :%s", nick, buf);

	return r;
} /* static int irc_notice_va(connection_type *conn, const char *nick,
		const char *format, va_list va) */



/*
 * Send data to all connected clients.
 */
int
irc_mnotice(clientlist_type *clients, const char *nick, const char *format, ...)
{
	llist_node *client;
	va_list va;
	int ret;

	if (clients->connected == 0) {
		return 0;
	}

	ret = 0;
	va_start(va, format);
	for (client = clients->clients->head; client != NULL;
			client = client->next) {
		va_list cva;
		va_copy(cva, va);
		ret += irc_notice_va((connection_type *) client->data,
				nick, format, cva);
		va_end(cva);
	}
	va_end(va);

	return ret;
} /* int irc_mnotice(clientlist_type *clients, const char *nick,
		const char *format, ...) */



void
irc_notice(connection_type *connection, const char *nick,
		const char *format, ...)
{
	va_list va;
	
	va_start(va, format);
	irc_notice_va(connection, nick, format, va);
	va_end(va);
} /* void irc_notice(connection_type *connection, const char *nick,
		const char *format, ...) */



/*
 * Read data.
 *
 * Return values:
 * 	-1	An error occured
 * 	0	No data
 * 	1	Received data
 * Returns number of bytes received, -1 if there was an error.
 */
int
irc_read(connection_type *connection)
{
	int	ret;

	do {
		ret = recv(connection->socket,
				connection->buffer + connection->offset, 1, 0);
		if (ret == 0) return -1;
		if (ret == -1 && errno == EAGAIN) return 0;
		connection->offset++;
		connection->timer = 0; /* Got data, reset timer. */
	} while (connection->buffer[connection->offset - 1] != '\n' &&
			connection->offset < BUFFERSIZE - 4);
	connection->buffer[connection->offset - 1] = '\0';
	if (connection->buffer[connection->offset - 2] == '\r') {
		connection->buffer[connection->offset - 2] = '\0';
	}
	connection->buffer[BUFFERSIZE - 1] = '\0';

#ifdef DEBUG
	fprintf(stdout, "<<%03d<< %s\n", connection->socket,
			connection->buffer);
	fflush(stdout);
#endif
	connection->offset = 0;
	return 1;
} /* int irc_read(connection_type *connection) */



/*
 * Connect to IRC-server.
 *
 * Return values:
 *	CONN_OK		All ok
 *	CONN_SOCK	Can't create socket
 *	CONN_LOOKUP	Can't resolve address
 *	CONN_BIND	Can't bind port
 *	CONN_CONNECT	Can't connect
 *	CONN_WRITE	Couldn't send data
 *	CONN_OTHER	Setting to nonblocking failed
 */
int
irc_connect(connection_type *connection, server_type *server, char *nick,
		char *username, char *realname, char *bindto)
{
	int		randport = 0;
	int		ri, attempts;

	connection->timer = 0;
	connection->offset = 0;

	if ((connection->socket = sock_open()) < 0) {
		return CONN_SOCK;
	}

	hostinfo = 0;
	if (bindto) {
		if (name_lookup(bindto) == NULL) {
			return CONN_BIND;
		}
	}
	
	attempts = 15;
	do {
		/* random() here is totally safe */
		for (ri = random() & 0xff; ri; ri--) {
			randport = (random() & 0xffff) | 1024;
		}
#ifdef IPV6
		addr.sin6_port = htons(randport);
#else
		addr.sin_port = htons(randport);
#endif
		attempts--;
	} while (! sock_bindlookedup(connection->socket, randport) && attempts);
	
	if (! attempts) {
		return CONN_BIND;
	}

	if (name_lookup(server->name) == NULL) {
		return CONN_LOOKUP;
	}

#ifdef IPV6
	memcpy((char *) &addr.sin6_addr, hostinfo->h_addr, hostinfo->h_length);
	addr.sin6_port = htons((u_short) server->port);
	addr.sin6_family = hostinfo->h_addrtype;
	if (connect(connection->socket, (struct sockaddr *) &addr,
				sizeof(struct sockaddr_in6)) < 0) {
		return CONN_CONNECT;
	}
#else
	memcpy((char *) &addr.sin_addr, hostinfo->h_addr, hostinfo->h_length);
	addr.sin_port = htons((u_short) server->port);
	
	addr.sin_family = hostinfo->h_addrtype;
	if (connect(connection->socket, (struct sockaddr *) &addr,
				sizeof(struct sockaddr_in)) < 0) {
		return CONN_CONNECT;
	}
#endif

	if (server->password) {
		irc_write(connection, "PASS %s", server->password);
	}
	if (irc_write(connection, "NICK %s", nick) < 0) {
		return CONN_WRITE;
	}
	/* We be lazy - modes not set here. */
	if (irc_write(connection, "USER %s 0 * :%s", username, realname) < 0) {
		return CONN_WRITE;
	}
	if (! sock_setnonblock(connection->socket)) {
		return CONN_OTHER;
	}
	
	return CONN_OK;
} /* int irc_connect(connection_type *connection, server_type *server,
		char *nickname, char *username, char *realname, char *bindto) */


syntax highlighted by Code2HTML, v. 0.9.1