/* X-Chat
 * Copyright (C) 1998 Peter Zelezny.
 *
 * 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.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include "xchat.h"
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "plugin.h"
#include "fe.h"

#ifdef USE_OPENSSL
#include <openssl/ssl.h>		  /* SSL_() */
#include <openssl/err.h>		  /* ERR_() */
#include "ssl.h"
#endif

extern GSList *sess_list;
extern struct xchatprefs prefs;

extern int handle_command (char *cmd, struct session *sess, int history, int);
extern void auto_reconnect (struct server *serv, int send_quit, int err);
extern void clear_channel (struct session *sess);
extern void set_server_name (struct server *serv, char *name);
extern void flush_server_queue (struct server *serv);
extern int tcp_send_len (struct server *serv, char *buf, int len);
extern int tcp_send (struct server *serv, char *buf);
extern void PrintText (struct session *sess, unsigned char *text);
extern void read_data (struct server *serv, gint sok);
extern char *errorstring (int err);
extern int waitline (int sok, char *buf, int bufsize);
extern void notc_msg (struct session *sess);
extern void notify_cleanup (void);

void disconnect_server (struct session *sess, int sendquit, int err);

static void
server_cleanup (server * serv)
{
	if (serv->iotag != -1)
	{
		fe_input_remove (serv->iotag);
		serv->iotag = -1;
	}
	close (serv->childwrite);
	close (serv->childread);
	waitpid (serv->childpid, NULL, 0);
#ifdef USE_OPENSSL
	if (serv->ssl_do_connect_tag != -1)
	{
		fe_timeout_remove (serv->ssl_do_connect_tag);
		serv->ssl_do_connect_tag = -1;
	}
#endif
	fe_progressbar_end (serv->front_session);
	serv->connecting = 0;
}

static void
server_connected (server * serv)
{
	char outbuf[512];

	serv->ping_recv = time (0);

	sprintf (outbuf, "%s/auth/xchat_auth", g_get_home_dir ());
	if (access (outbuf, X_OK) == 0)
	{
		sprintf (outbuf, "/exec %s/auth/xchat_auth %s", g_get_home_dir (),
					prefs.username);
		handle_command (outbuf, serv->front_session, FALSE, FALSE);
	}

	serv->connected = TRUE;
	serv->iotag = fe_input_add (serv->sok, 1, 0, 1, read_data, serv);
	if (!serv->no_login)
	{
		EMIT_SIGNAL (XP_TE_CONNECTED, serv->front_session, NULL, NULL, NULL,
						 NULL, 0);
		if (serv->password[0])
		{
			sprintf (outbuf, "PASS %s\r\n", serv->password);
			tcp_send (serv, outbuf);
		}
		snprintf (outbuf, sizeof (outbuf) - 1,
					 "NICK %s\r\n"
					 "USER %s localhost %s :%s\r\n",
					 serv->nick, prefs.username, serv->servername, prefs.realname);
		tcp_send (serv, outbuf);
	} else
	{
		EMIT_SIGNAL (XP_TE_SERVERCONNECTED, serv->front_session, NULL, NULL,
						 NULL, NULL, 0);
	}
	fcntl (serv->sok, F_SETFL, O_NONBLOCK);
	set_server_name (serv, serv->servername);
}

#ifdef USE_OPENSSL
#define	SSLTMOUT	10	/* seconds */
struct session *g_sess = NULL;
void
ssl_cb_info (SSL * s, int where, int ret)
{
	char buf[128];


	return;		/* FIXME: make debug level adjustable in serverlist or settings */

	snprintf (buf, sizeof (buf), "%s (%d)", SSL_state_string_long (s), where);
	if (g_sess)
		EMIT_SIGNAL (XP_TE_SERVTEXT, g_sess, buf, NULL, NULL, NULL, 0);
	else
		fprintf (stderr, "%s\n", buf);
}

int
ssl_cb_verify (int ok, X509_STORE_CTX * ctx)
{
	char subject[256];
	char issuer[256];
	char buf[512];


	X509_NAME_oneline (X509_get_subject_name (ctx->current_cert), subject,
							 sizeof (subject));
	X509_NAME_oneline (X509_get_issuer_name (ctx->current_cert), issuer,
							 sizeof (issuer));

	snprintf (buf, sizeof (buf), "* Subject: %s", subject);
	EMIT_SIGNAL (XP_TE_SERVTEXT, g_sess, buf, NULL, NULL, NULL, 0);
	snprintf (buf, sizeof (buf), "* Issuer: %s", issuer);
	EMIT_SIGNAL (XP_TE_SERVTEXT, g_sess, buf, NULL, NULL, NULL, 0);

	return (TRUE);	/* always ok */
}

int
ssl_do_connect (server * serv)
{
	char buf[128];


	g_sess = serv->front_session;
	if (SSL_connect (serv->ssl) <= 0)
	{
		char err_buf[128];
		int err;

		g_sess = NULL;
		if ((err = ERR_get_error ()) > 0)
		{
			ERR_error_string (err, err_buf);
			snprintf (buf, sizeof (buf), "(%d) %s", err, err_buf);
			EMIT_SIGNAL (XP_TE_CONNFAIL, serv->front_session, buf, NULL,
							 NULL, NULL, 0);

			_SSL_close (serv->ssl);	/* free serv->ssl */
			close (serv->sok);	  /* close server connection */
			server_cleanup (serv); /* ->connecting = FALSE */

			if (prefs.autoreconnectonfail)
				auto_reconnect (serv, FALSE, -1);

			return (0);				  /* remove it (0) */
		}
	}
	g_sess = NULL;

	if (SSL_is_init_finished (serv->ssl))
	{
		int verify_error;

		verify_error = SSL_get_verify_result(serv->ssl);
		switch (verify_error) {
		    case X509_V_OK:
			snprintf (buf, sizeof (buf), "* Verify OK (?)");
			EMIT_SIGNAL (XP_TE_SERVTEXT, serv->front_session, buf, NULL, NULL, NULL, 0);
			break;
		    case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
		    case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
			if (serv->accept_invalid_cert) {
				snprintf (buf, sizeof (buf), "* Verify error: %s.? (%d) -- Ignored",
					X509_verify_cert_error_string(verify_error), verify_error);
				EMIT_SIGNAL (XP_TE_SERVTEXT, serv->front_session, buf, NULL, NULL, NULL, 0);
				break;
			}
		    default:
			snprintf (buf, sizeof (buf), "%s.? (%d)",
				X509_verify_cert_error_string(verify_error), verify_error);
			EMIT_SIGNAL (XP_TE_CONNFAIL, serv->front_session, buf, NULL,
							 NULL, NULL, 0);

			_SSL_close (serv->ssl);	/* free serv->ssl */
			close (serv->sok);	  /* close server connection */
			server_cleanup (serv); /* ->connecting = FALSE */

			return (0);
		}

		server_cleanup (serv);	  /* ->connecting = FALSE */

		EMIT_SIGNAL (XP_TE_SERVTEXT, serv->front_session,
						 _SSL_get_cipher_string (serv->ssl), NULL, NULL, NULL, 0);
		EMIT_SIGNAL (XP_TE_SERVTEXT, serv->front_session,
						 _SSL_get_cert_string (serv->ssl), NULL, NULL, NULL, 0);

		/* activate gtk poll */
		server_connected (serv);

		return (0);					  /* remove it (0) */
	} else
	{
		if (serv->ssl->session->time + SSLTMOUT < time (NULL))
		{
			snprintf (buf, sizeof (buf), "SSL handshake timed out");
			EMIT_SIGNAL (XP_TE_CONNFAIL, serv->front_session, buf, NULL,
							 NULL, NULL, 0);
			_SSL_close (serv->ssl);	/* free serv->ssl */
			close (serv->sok);	  /* close server connection */
			server_cleanup (serv); /* ->connecting = FALSE */

			if (prefs.autoreconnectonfail)
				auto_reconnect (serv, FALSE, -1);

			return (0);				  /* remove it (0) */
		}

		return (1);					  /* call it more (1) */
	}
}
#endif

static void
connected_signal (server * serv, int sok)
{
	session *sess = serv->front_session;
	char tbuf[128];
	char outbuf[512];
	char host[100];
	char ip[100];

	waitline (serv->childread, tbuf, sizeof tbuf);

	switch (tbuf[0])
	{
	case '1':						  /* unknown host */
		server_cleanup (serv);
		close (sok);
		EMIT_SIGNAL (XP_TE_UKNHOST, sess, NULL, NULL, NULL, NULL, 0);
		break;
	case '2':						  /* connection failed */
		waitline (serv->childread, tbuf, sizeof tbuf);
		server_cleanup (serv);
		close (sok);
		EMIT_SIGNAL (XP_TE_CONNFAIL, sess, errorstring (atoi (tbuf)), NULL,
						 NULL, NULL, 0);
		if (prefs.autoreconnectonfail)
			auto_reconnect (serv, FALSE, -1);
		break;
	case '3':						  /* gethostbyname finished */
		waitline (serv->childread, host, sizeof host);
		waitline (serv->childread, ip, sizeof ip);
		waitline (serv->childread, outbuf, sizeof outbuf);
		EMIT_SIGNAL (XP_TE_CONNECT, sess, host, ip, outbuf, NULL, 0);
		strcpy (serv->hostname, host);
		break;
	case '4':						  /* success */
#ifdef USE_OPENSSL
#define	SSLDOCONNTMOUT	300
		if (serv->use_ssl)
 		{
			char *err;
 
 			/* it'll be a memory leak, if connection doesn't terminated by
 			   server_disconnect() */
 			serv->ssl = _SSL_socket (serv->sok);
 			if ((err = _SSL_set_verify (ssl_cb_verify, NULL)))
  			{
 				EMIT_SIGNAL (XP_TE_CONNFAIL, serv->front_session, err, NULL,
 								 NULL, NULL, 0);
 
 				_SSL_close (serv->ssl);	/* free serv->ssl */
 				close (serv->sok);  /* close server connection */
 				server_cleanup (serv);	/* ->connecting = FALSE */
 				return;
 			}
 
 			/* FIXME: it'll be needed by new servers */
 			/* send(serv->sok, "STLS\r\n", 6, 0); sleep(1); */
 
 			fcntl (serv->sok, F_SETFL, O_NONBLOCK);
 
 			if (
 				 (serv->ssl_do_connect_tag =
 				  fe_timeout_add (SSLDOCONNTMOUT, ssl_do_connect, (void *) serv)) == -1)
 			{
 				fprintf (stderr, "FATAL ERROR: fe_timeout_add() failed\n");
 				exit (1);
 			}
 		} else
 		{
			serv->ssl = NULL;
#endif
 			server_cleanup (serv); /* ->connecting = FALSE */
			/* activate gtk poll */
			server_connected (serv);
#ifdef USE_OPENSSL
		}
#endif
		break;
	case '5':						  /* prefs ip discovered */
		waitline (serv->childread, tbuf, sizeof tbuf);
		prefs.local_ip = inet_addr (tbuf);
		break;
	case '7':						  /* gethostbyname (prefs.hostname) failed */
		sprintf (outbuf, "Cannot resolv hostname %s\nCheck your IP Settings!",
					prefs.hostname);
		PrintText (sess, outbuf);
		break;
	case '8':
		PrintText (sess, "Socks traversal failed.\n");
		disconnect_server (sess, FALSE, -1);
		break;
	case '9':
		waitline (serv->childread, tbuf, sizeof tbuf);
		EMIT_SIGNAL (XP_TE_SERVERLOOKUP, sess, tbuf, NULL, NULL, NULL, 0);
		break;
	}
}

static int
check_connecting (struct session *sess)
{
	char tbuf[256];

	if (sess->server->connecting)
	{
		kill (sess->server->childpid, SIGKILL);
		sprintf (tbuf, "%d", sess->server->childpid);
		EMIT_SIGNAL (XP_TE_SCONNECT, sess, tbuf, NULL, NULL, NULL, 0);
		server_cleanup (sess->server);
#ifdef USE_OPENSSL
		if (sess->server->ssl)	  /* maybe, if we're waiting for child's connect() */
			_SSL_close (sess->server->ssl);
#endif
		close (sess->server->sok);
		return TRUE;
	}
	return FALSE;
}

int
close_socket (int sok)
{
	close (sok);
	return 0;
}

void
disconnect_server (struct session *sess, int sendquit, int err)
{
	struct session *orig_sess = sess;
	struct server *serv = sess->server;
	GSList *list;

	if (check_connecting (sess))
		return;

	/* is this server in a reconnect delay? remove it! */
	if (serv->recondelay_tag != -1)
	{
		fe_input_remove (serv->recondelay_tag);
		serv->recondelay_tag = -1;
	} else
	{
		if (!serv->connected)
		{
			notc_msg (sess);
			return;
		}
	}

	flush_server_queue (serv);

	if (serv->iotag != -1)
	{
		fe_input_remove (serv->iotag);
		serv->iotag = -1;
	}

	list = sess_list;
	while (list)					  /* print "Disconnected" to each window using this server */
	{
		sess = (struct session *) list->data;
		if (sess->server == serv)
			EMIT_SIGNAL (XP_TE_DISCON, sess, errorstring (err), NULL, NULL, NULL,
							 0);
		list = list->next;
	}

	/* when removing reconnect delay only */
	if (serv->sok == -1)
		return;

	sess = orig_sess;
	if (sendquit)
	{
		char tbuf[256];
		if (!sess->quitreason)
			sess->quitreason = prefs.quitreason;
		snprintf (tbuf, sizeof tbuf, "QUIT :%s\r\n", sess->quitreason);
		sess->quitreason = 0;
		tcp_send (serv, tbuf);
	}

	/* close it in 5 seconds so the QUIT doesn't get lost. Technically      *
	 * we should send a QUIT and then wait for the server to disconnect us, *
	   but that would hold up the GUI                                       */
	fe_timeout_add (5000, close_socket, (void *) serv->sok);

#ifdef USE_OPENSSL
	if (serv->ssl)
		_SSL_close (serv->ssl);
#endif

	serv->sok = -1;
	serv->pos = 0;
	serv->connected = FALSE;
	serv->motd_skipped = FALSE;
	serv->no_login = FALSE;
	serv->servername[0] = 0;

	list = sess_list;
	while (list)
	{
		sess = (struct session *) list->data;
		if (sess->server == serv)
		{
			if (sess->channel[0])
			{
				if (!sess->is_dialog && !sess->is_server)
				{
					clear_channel (sess);
				}
			} else
			{
				clear_channel (sess);
			}
		}
		list = list->next;
	}
	notify_cleanup ();
}

struct sock_connect
{
	char version;
	char type;
	unsigned short port;
	unsigned long address;
	char username[10];
};

/* traverse_socks() returns:
 *				0 success                *
 *          1 socks traversal failed */

static int
traverse_socks (int sok, unsigned long serverAddr, int port)
{
	struct sock_connect sc;
	unsigned char buf[10];

	sc.version = 4;
	sc.type = 1;
	sc.port = htons (port);
	sc.address = serverAddr;
	strncpy (sc.username, prefs.username, 9);

	send (sok, &sc, 8 + strlen (sc.username) + 1, 0);
	buf[1] = 0;
	recv (sok, buf, 10, 0);
	if (buf[1] == 90)
		return 0;

	return 1;
}

struct sock5_connect1
{
       char version;
       char nmethods;
       char method;
};

struct sock5_connect2
{
       char version;
       char cmd;
       char rsv;
       char atyp;
       unsigned long address;
       unsigned short port;
};

static int
traverse_socks5 (int sok, unsigned long serverAddr, int port)
{
       struct sock5_connect1 sc1;
       struct sock5_connect2 sc2;
       unsigned char buf[10];

       sc1.version = 5;
       sc1.nmethods = 1;
       sc1.method = 0;
       send (sok, &sc1, 3, 0);
       recv (sok, buf, 2, 0);
       if (buf[0] != 5 && buf[1] != 0)
               return 1;

       sc2.version = 5;
       sc2.cmd = 1;
       sc2.rsv = 0;
       sc2.atyp = 1;
       sc2.address = serverAddr;
       sc2.port = htons (port);
       
       send (sok, &sc2, 10, 0);
       recv (sok, buf, 10, 0);
       if (buf[0] != 5 && buf[1] != 0)
               return 1;

       return 0;
}

static int
traverse_wingate (int sok, unsigned long serverAddr, int port)
{
	char buf[128];
	struct in_addr in;

	in.s_addr = serverAddr;
 
	snprintf (buf, sizeof (buf), "%s %d\r\n", inet_ntoa (in), port);
	send (sok, buf, strlen (buf), 0);

	return 0;
}

static int
traverse_proxy (int sok, unsigned long serverAddr, int port)
{
	switch (prefs.proxy_type)
	{
	case 1:
		return traverse_wingate (sok, serverAddr, port);
	case 2:
		return traverse_socks (sok, serverAddr, port);
	case 3:
		return traverse_socks5 (sok, serverAddr, port);
	}

	return 1;
}

void
connect_server (struct session *sess, char *server, int port, int no_login)
{
	int sok, sw, pid, read_des[2];

	if (!server[0])
		return;

	sess = sess->server->front_session;

	if (sess->server->connected)
		disconnect_server (sess, TRUE, -1);
	else
		check_connecting (sess);

	fe_progressbar_start (sess);

	EMIT_SIGNAL (XP_TE_SERVERLOOKUP, sess, server, NULL, NULL, NULL, 0);

	sok = socket (AF_INET, SOCK_STREAM, 0);
	if (sok == -1)
		return;

	sw = 1;
	setsockopt (sok, SOL_SOCKET, SO_REUSEADDR, (char *) &sw, sizeof (sw));
	sw = 1;
	setsockopt (sok, SOL_SOCKET, SO_KEEPALIVE, (char *) &sw, sizeof (sw));

	strcpy (sess->server->servername, server);
	sess->server->nickcount = 1;
	sess->server->connecting = TRUE;
	sess->server->sok = sok;
	sess->server->port = port;
	sess->server->end_of_motd = FALSE;
	sess->server->no_login = no_login;
	flush_server_queue (sess->server);

	if (pipe (read_des) < 0)
		return;
#ifdef __EMX__						  /* if os/2 */
	setmode (read_des[0], O_BINARY);
	setmode (read_des[1], O_BINARY);
#endif

	switch (pid = fork ())
	{
	case -1:
		return;

	case 0:
		{
			struct sockaddr_in DSAddr;	/* for binding our local hostname. */
			struct sockaddr_in SAddr;	/* for server connect. */
			struct hostent *HostAddr;
			unsigned long serverAddr;
			int proxy = FALSE;

			dup2 (read_des[1], 1); /* make the pipe our stdout */
			setuid (getuid ());

			/* is a hostname set at all? */
			if (prefs.hostname[0])
			{
				HostAddr = gethostbyname (prefs.hostname);
				if (HostAddr)
				{
					memset (&DSAddr, 0, sizeof (DSAddr));
					memcpy ((void *) &DSAddr.sin_addr, HostAddr->h_addr,
							  HostAddr->h_length);
					printf ("5\n%s\n", inet_ntoa (DSAddr.sin_addr));
					bind (sok, (struct sockaddr *) &DSAddr, sizeof (DSAddr));
				} else
				{
					printf ("7\n");
				}
				fflush (stdout);
			}

			/* resolve server name (not the proxy) */
			HostAddr = gethostbyname (server);
			if (!HostAddr)
			{
				printf ("1\n");
				fflush (stdout);
				_exit (0);
			}
			memcpy (&serverAddr, HostAddr->h_addr, 4);

			if (prefs.proxy_host[0] && prefs.proxy_type > 0)
			{
				proxy = TRUE;
				printf ("9\n%s\n", prefs.proxy_host);
				HostAddr = gethostbyname (prefs.proxy_host);
				if (!HostAddr)
				{
					printf ("1\n");
					fflush (stdout);
					_exit (0);
				}
			}

			printf ("3\n%s\n%s\n%d\n", HostAddr->h_name,
					  inet_ntoa (*((struct in_addr *) HostAddr->h_addr)),
						proxy ? prefs.proxy_port : port);
			fflush (stdout);
			memset (&SAddr, 0, sizeof (SAddr));
			if (proxy)
				SAddr.sin_port = htons (prefs.proxy_port);
			else
				SAddr.sin_port = htons (port);
			SAddr.sin_family = AF_INET;
			memcpy ((void *) &SAddr.sin_addr, HostAddr->h_addr,
					  HostAddr->h_length);
			if (connect (sok, (struct sockaddr *) &SAddr, sizeof (SAddr)) < 0)
				printf ("2\n%d\n", errno);
			else
			{	/* connect succeeded */
				if (proxy)
				{
					switch (traverse_proxy (sok, serverAddr, port))
					{
					case 0:
						printf ("4\n");		/* success */
						break;
					case 1:
						printf ("8\n");		/* socks traversal failed */
						break;
					}
				} else
				{
					printf ("4\n");
				}
			}
			fflush (stdout);
			_exit (0);
		}
	}
	sess->server->childpid = pid;
	sess->server->iotag = fe_input_add (read_des[0], 1, 0, 0,
													connected_signal, sess->server);
	sess->server->childread = read_des[0];
	sess->server->childwrite = read_des[1];
}
