/* irc proxy
 * (c) 2004 gophi@linux.net.pl
 * 
 * najprościej mówiąc irc proxy wpuszczające autoryzując po ip i idencie, 
 * posiadające możliwość łączenia się z ircserwerem po ipv4, ipv6 oraz 
 * ustalenia własnego vhosta v4/v6. do tego mały skrypt basha do crona:
 *
 * ps -cx | awk '{print $5}' > .irccron.tmp
 * grep ircproxy .irccron.tmp > .irccron.tmp2
 * [ -s .irccron.tmp2 ] || bin/ircproxy
 * rm -f .irccron.tmp{,2}
 *
 * tak, wiem, pgrep, pidof, itp., ale ma być uniwersalne. grep podzielony
 * na dwie linijki, żeby przy dużej liczbie procesów nie był zinterpretowany
 * jako jeden z procesów, których szukamy.
 */

#include <signal.h>
#include <syslog.h>
#include <stdio.h>
#include <errno.h>
#include <netdb.h>
#include <unistd.h>
#include <pwd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <stdarg.h>
#include <time.h>

// #define SERVER "irc.fu-berlin.de"
#define SERVER "warszawa6.irc.pl"
#define VHOST "gophi.net"
#define SERVER_PORT 6667
#define ALLOW_FROM "host.gophiego"
#define ALLOW_AUTH "gophi"
#define BIND_PORT 6667
#define IDENT_TIMEOUT 20
#define MAX_FORKS 10

char *server = SERVER;
char *vhost = VHOST;
int server_port = SERVER_PORT;
char *allow_from = ALLOW_FROM;
char *allow_auth = ALLOW_AUTH;
int bind_port = BIND_PORT;
int ident_timeout = IDENT_TIMEOUT;
int max_forks = MAX_FORKS;
char do_daemonize = 1, do_dup = 1;
char only_v4 = 0, only_v6 = 0;

extern int errno;
extern char *optarg;
extern int optind, opterr, optopt;

volatile int num_forks = 0;
int bind_fd;
struct in_addr allow_addr;
struct addrinfo *server_aitop, *vhost_aitop;

void hnd_term (int signo)
{
	if (bind_fd)
		close (bind_fd);

	_exit (0);
}

void hnd_alrm (int signo)
{
	_exit (1);
}

void hnd_chld (int signo)
{
	while (waitpid(-1, NULL, WNOHANG) > 0);

	if (num_forks)
		num_forks--;

	signal (SIGCHLD, hnd_chld);
}

void panic (char *str)
{
	perror (str);
	_exit (1);
}

void debug (const char *fmt, ...)
{
	va_list args;
	size_t len;
	char buf[256];

	va_start (args, fmt);
	vsnprintf (buf, sizeof(buf), fmt, args);
	va_end (args);

	fprintf (stderr, "debug: %s\n", buf);
}

int prep_socket (void)
{
	struct sockaddr_in addr;
	struct in_addr in_address;
	int fd;

	debug ("próba słuchania na porcie %d.", bind_port);
	fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (fd < 0)
		panic ("socket");

	addr.sin_addr.s_addr = INADDR_ANY;
	addr.sin_port = htons(bind_port);
	addr.sin_family = AF_INET;

	if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
		panic ("bind");

	if (listen(fd, 10) < 0)
		panic ("listen");

	return fd;
}

void daemonize (void)
{
	FILE *fp;
	int rs = 0;

	debug ("przechodzenie w tło.");

	switch (fork()) {
		case 0:
			break;
		case -1:
			panic ("fork");
		default:
			_exit (0);
	}

	if (setsid() < 0)
		panic ("setsid");

	if (chdir("/"))
		panic ("chdir");

	if (do_dup) {
		fp = fopen("/dev/null", "w+");
		if (!fp)
			panic ("/dev/null");

		rs |= dup2(fileno(fp), 0) == -1 ? 1 : 0;
		rs |= dup2(fileno(fp), 1) == -1 ? 1 : 0;
		rs |= dup2(fileno(fp), 2) == -1 ? 1 : 0;

		if (rs)
			panic ("dup2");

		fclose (fp);
	}
}

void read_line (int fd, char *buf, int buf_len)
{
	int i = 0, num_read;
	char ch, done = 0;

	while (!done) {
		num_read = read(fd, &ch, 1);
		if (!num_read)
			done = 1;
		else switch (ch) {
			case 0x0D:
				break;
			case 0x0A:
				done = 1;
				break;
			default:
				if (i < buf_len - 2)
					buf[i++] = ch;
				else
					done = 1;
				break;
		}
	}

	buf[i] = (char) NULL;
}

void kill_conn (int fd, char *str)
{
	char buf[1024];

	snprintf (buf, sizeof(buf), "ERROR :%s\r\n", str);
	write (fd, buf, strlen(buf));
	close (fd);
}

void check_ident (int fd, unsigned short src_port)
{
	int ident_fd;
	struct sockaddr_in addr;
	int rs;
	char buf[40];
	char code[sizeof(buf)], user[sizeof(buf)];

	debug ("sprawdzanie autoryzacji.");

	alarm (ident_timeout);
	signal (SIGALRM, hnd_alrm);

	ident_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (ident_fd < 0)
		_exit (1);

	addr.sin_addr.s_addr = allow_addr.s_addr;
	addr.sin_port = htons(113);
	addr.sin_family = AF_INET;

	snprintf (buf, sizeof(buf), "%d , %d\n", src_port, bind_port);

	rs = connect(ident_fd, (struct sockaddr *) &addr, sizeof(addr));
	if (rs < 0) {
		kill_conn (fd, "Błąd wewnętrzny.");
		_exit (1);
	}

	debug ("połączono z serwerem autoryzacji.");

	write (ident_fd, buf, strlen(buf));
	read_line (ident_fd, buf, sizeof(buf));
	close (ident_fd);

	sscanf (buf, "%*d , %*d : %s : %*s : %s", code, user);

	debug ("stan autoryzacji: %s.", code);
	debug ("użytkownik: %s.", user);

	if (strcasecmp(code, "userid")) {
		kill_conn (fd, "Niewłaściwy stan autoryzacji.");
		debug ("niewłaściwy stan.");
		_exit (1);
	}

	if (strcmp(user, allow_auth)) {
		kill_conn (fd, "Odmowa autoryzacji.");
		debug ("odmowa autoryzacji.\n");
		_exit (1);
	}

	debug ("autoryzacja przebiegła pomyślnie.");

	alarm (0);
	signal (SIGALRM, SIG_IGN);
}

char pass_data (int srcfd, int dstfd)
{
	int num;
	char buf[16384];

	num = read(srcfd, buf, sizeof(buf));
	if (!num)
		return 1;

	write (dstfd, buf, num);

	return 0;
}

void hnd_conn (int fd, unsigned short src_port)
{
	int srv_fd, rs;
	struct sockaddr_in addr;
	char done = 0;
	fd_set rdfds;
	int i, maxfd = 0;
	struct addrinfo *ai;
	char ntop[NI_MAXHOST];
	char strport[NI_MAXSERV];
	char have_conn = 0;

	check_ident (fd, src_port);

	snprintf (strport, sizeof(strport), "%u", server_port);

	for (ai = server_aitop; ai; ai = ai->ai_next) {
		if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6)
			continue;

		if (getnameinfo(ai->ai_addr, ai->ai_addrlen, ntop, sizeof(ntop), strport, sizeof(strport), 
			NI_NUMERICHOST | NI_NUMERICSERV) != 0) {
			kill_conn (fd, "Błąd wewnętrzny getnameinfo().");
			debug ("getnameinfo(): error.");
			_exit (1);
		}

		srv_fd = socket(ai->ai_family, SOCK_STREAM, IPPROTO_TCP);
		if (srv_fd < 0) {
			kill_conn (fd, "Błąd wewnętrzny socket().");
			panic ("socket");
		}

		if (strcmp(vhost, "none")) {
			debug ("ustawianie vhosta %s.", vhost);
			if (bind(srv_fd, vhost_aitop->ai_addr, vhost_aitop->ai_addrlen) < 0) {
				kill_conn (fd, "Błąd wewnętrzny bind().");
				panic ("bind");
			}
		}

		debug ("łączenie z %s [%s] (port %d).", server, ntop, server_port);

		if (connect(srv_fd, ai->ai_addr, ai->ai_addrlen) >= 0) {
			have_conn = 1;
			break;
		}

		debug ("nieudane połączenie.");
		close (srv_fd);
	}

	if (!have_conn) {
		kill_conn (fd, "Nieudane połączenie.");
		_exit (1);
	}

	debug ("ustanowiono połączenie z %s.", server);

	if (srv_fd > fd)
		maxfd = srv_fd + 1;
	else
		maxfd = fd + 1;

	debug ("%s@%s podłączony do %s:%d.", allow_auth, allow_from, server, server_port);

	while (!done) {
		FD_ZERO (&rdfds);
		FD_SET (fd, &rdfds);
		FD_SET (srv_fd, &rdfds);

		rs = select(maxfd, &rdfds, NULL, NULL, NULL);
		if (rs < 0) {
			debug ("błąd sprawdzania stanu gniazda.");
			_exit (1);
		} else if (!rs)
			continue;

		if (FD_ISSET(fd, &rdfds))
			done = pass_data(fd, srv_fd);

		if (FD_ISSET(srv_fd, &rdfds))
			done = pass_data(srv_fd, fd);
	}

	debug ("%s@%s odłączony od %s:%d.", allow_auth, allow_from, server, server_port);

	close (fd);
	close (srv_fd);
}

void fasthelp (void)
{
	printf ("opcje:\n\n");
	printf (" -s host: łączy z podanym serwerem (domyślnie %s).\n", SERVER);
	printf (" -p port: łączy na podanym porcie (domyślnie %d).\n", SERVER_PORT);
	printf (" -v host: wchodzi z podanego vhosta (domyślnie %s). Ustawienie \"none\" wyłącza vhosta.\n", VHOST);
	printf (" -a host: umożliwia dostęp z podanego hosta (domyślnie %s).\n", ALLOW_FROM);
	printf (" -i auth: umożliwia dostęp z podanego identa (domyślnie %s).\n", ALLOW_AUTH);
	printf (" -b port: binduje się do podanego portu (domyślnie %d).\n", BIND_PORT);
	printf (" -t czas: ustawia czas czekania na odpowiedź auth (domyślnie %d sekund).\n", IDENT_TIMEOUT);
	printf (" -f liczba: określa, ile można sforkować procesów (domyślnie %d).\n", MAX_FORKS);
	printf (" -n: wyświetlanie komunikatów także po przejściu w tło.\n");
	printf (" -4: wyłącznie łączenie po ipv4.\n");
	printf (" -6: wyłącznie łączenie po ipv6.\n");
}

void parse_options (int argc, char **argv)
{
	const char options[] = "s:p:a:i:b:t:f:v:dhn64";
	int optret;
	char done = 0;

	while (!done) {
		optret = getopt(argc, argv, options);
		switch (optret) {
			case ':':
				debug ("brak parametru w linii poleceń.");
				done = 3;
				break;
			case '?':
				debug ("nieznana opcja w linii poleceń.");
				done = 3;
				break;
			case -1:
				done = 1;
				break;
			case 'h':
				fasthelp();
				done = 2;
				break;
			case 's':
				server = optarg;
				break;
			case 'p':
				server_port = atoi(optarg);
				break;
			case 'v':
				vhost = optarg;
				break;
			case 'a':
				allow_from = optarg;
				break;
			case 'i':
				allow_auth = optarg;
				break;
			case 'b':
				bind_port = atoi(optarg);
				break;
			case 't':
				ident_timeout = atoi(optarg);
				break;
			case 'f':
				max_forks = atoi(optarg);
				break;
			case 'd':
				do_daemonize = 0;
				break;
			case 'n':
				do_dup = 0;
				break;
			case '4':
				if (!only_v6)
					only_v4 = 1;
				else {
					debug ("opcje -6 i -4 wzajemnie się wykluczają.");
					done = 3;
				}
				break;
			case '6':
				if (!only_v4)
					only_v6 = 1;
				else {
					debug ("opcje -4 i -6 wzajemnie się wykluczają.");
					done = 3;
				}
				break;
		}
	}

	if (done > 1)
		_exit (done - 2);
}

void dump_options (void)
{
	static char *v46[] = {"najpierw v6, potem v4", "tylko v6", "tylko v4", "błąd wewnętrzny?"};

	debug ("serwer: %s:%d.", server, server_port);
	debug ("klient: %s@%s.", allow_auth, allow_from);
	debug ("vhost: %s.", vhost);
	debug ("port na którym słucham: %d.", bind_port);
	debug ("czas oczekiwania na autoryzację: %d sekund.", ident_timeout);
	debug ("maksymalna liczba forków: %d.", max_forks);
	debug ("przechodzenie w tło: %s.", do_daemonize ? "tak" : "nie");
	debug ("odłączanie stderr: %s.", do_dup ? "tak" : "nie");
	debug ("preferowana droga łączenia: %s.", v46[(only_v4 << 1) | only_v6]);
}

char *my_hostname (struct addrinfo *ai)
{
	static char ntop[NI_MAXHOST];

	if (getnameinfo(ai->ai_addr, ai->ai_addrlen, ntop, sizeof(ntop), NULL, NULL, 
		NI_NUMERICHOST) != 0) {
		debug ("błąd getnameinfo()");
		_exit (1);
	}

	return ntop;
}

int main (int argc, char **argv)
{
	int conn_fd;
	struct sockaddr_in conn_addr;
	socklen_t addr_sz = sizeof(struct sockaddr_in);
	struct addrinfo hints;
	struct hostent *hent;
	char strport[NI_MAXSERV];
	int gai_err;

	parse_options (argc, argv);
	dump_options();

	debug ("ustalanie adresu serwera %s.", server);
	bzero (&hints, sizeof(hints));
	if (only_v4)
		hints.ai_family = AF_INET;
	else if (only_v6)
		hints.ai_family = AF_INET6;
	else
		hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	snprintf (strport, sizeof(strport), "%u", server_port);
	gai_err = getaddrinfo(server, strport, &hints, &server_aitop);
	if (gai_err != 0) {
		debug ("getaddrinfo(): %s.", gai_strerror(gai_err));
		_exit (1);
	}

	debug ("serwer ma adres %s.", my_hostname(server_aitop));

	if (strcmp(vhost, "none")) {
		debug ("ustalanie adresu vhosta %s.", vhost);
		bzero (&hints, sizeof(hints));
		if (only_v4)
			hints.ai_family = AF_INET;
		else if (only_v6)
			hints.ai_family = AF_INET6;
		else
			hints.ai_family = AF_UNSPEC;
		hints.ai_socktype = SOCK_STREAM;
		hints.ai_protocol = IPPROTO_TCP;
		gai_err = getaddrinfo(vhost, "0", &hints, &vhost_aitop);
		if (gai_err != 0) {
			debug ("getaddrinfo(): %s.", gai_strerror(gai_err));
			_exit (1);
		}
		debug ("vhost ma adres %s.", my_hostname(vhost_aitop));
	}

	debug ("ustalanie adresu klienta %s.", allow_from);
	hent = gethostbyname(allow_from);
	if (!hent)
		panic ("gethostbyname");

	memcpy ((char *) &allow_addr, hent->h_addr, sizeof(allow_addr));
	debug ("klient ma adres %s.", (char *) inet_ntoa(allow_addr.s_addr));

	bind_fd = prep_socket();
	if (do_daemonize)
		daemonize();

	signal (SIGINT, hnd_term);
	signal (SIGTERM, hnd_term);
	signal (SIGCHLD, hnd_chld);

	debug ("czekanie na połączenie.");

	for (;;) {
		conn_fd = accept(bind_fd, (struct sockaddr *) &conn_addr, &addr_sz);
		if (conn_fd < 0)
			panic ("accept");

		debug ("połączenie spod %s.", (char *) inet_ntoa(conn_addr.sin_addr.s_addr));

		if (num_forks >= max_forks) {
			debug ("przekroczony limit forków.");
			close (conn_fd);
			continue;
		}

		if (conn_addr.sin_addr.s_addr != allow_addr.s_addr) {
			debug ("odmowa autoryzacji adresu.");
			kill_conn (conn_fd, "Odmowa autoryzacji adresu.");
			continue;
		} else
			debug ("poprawna autoryzacja adresu.");

		num_forks++;

		switch (fork()) {
			case 0:
				close (bind_fd);
				bind_fd = 0;
				hnd_conn (conn_fd, ntohs(conn_addr.sin_port));
				_exit (0);
			case -1:
				panic ("fork");
			default:
				close (conn_fd);
		}
	}

	/* NOTREACHED */

	return 0;
}
