wm: ticl

ref: 5ff9649e6c51bae0dd09f218e4943fa5f4d458be
dir: /main.c/

View raw version
/*
 * This work is dedicated to the public domain.
 * See COPYING file for more information.
 */

#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <unistd.h>

#include "utils.h"
#include "htable.h"

#define BUFFER_LEN	1024
#define MAX_FD		1024

#define NET_ADDEND	10
#define USER_ADDEND	100

#define FD_TYPE_LINKER	1
#define FD_TYPE_CLONE	2

typedef struct {
	int	 fd;	/* fd of linker	*/
	char	*name;	/* name		*/
	char	*symb;	/* symbol	*/
	char	*host;	/* hostname	*/
	char	*port;	/* port		*/
	char	*chan;	/* channel	*/
} Network;

static fd_set	 fdset;			/* fd_set (select)	*/
static int	 nfds;			/* number of fds	*/

static int	 fdnet	[MAX_FD];	/* fd -> network index	*/
static int	 fdtype	[MAX_FD];	/* fd -> fd type	*/
static int	 fdsuf	[MAX_FD];	/* fd -> nick suffix	*/

static Network	*networks;		/* linked list of networks */
static int	 netlen;		/* current network length  */
static int	 netcap = NET_ADDEND;	/* total memory allocated  */

/*
 * hash table of users
 * key   -> <nickname> + '[' + <network_symbol> + ']'
 * value -> array of linked clones fds indexed
 *	    corresponding to its connected network
 */
static Htable	 *users;	

static int	 isrunning = 1;

/* prototype */
void	 print_table(void);
void	 print_users(void);
int	 fdadd(char *host, char *port);
void	 fddel(int fd, char *msg);
void	 netadd(char *name, char *symb, char *host, char *port, char *chan);
void	 __netdel(int index);
void	 netdel(char *name);
int	 cloneadd(int netindex, char *nick);
void	 clonesupdate(void);
void	 useradd(char *unick, int nfd);
void	 userdel(char *nick, char *msg);
void	 append_netsymb(char *nick, int fd);
int	*getuserfds(char *nick, int nfd);
void	 privmsg_update(char *dst, char *src, int fd);
void	 handle_server(int fd);
void	 handle_fifo(int fd);

void
print_table(void)
{
	int i, *fds;
	Htiter	it = {0};

	if (netlen == 0) return;

	/* print networks */
	printf("Networks\t");
	for (i = 0; i < netlen; i++)
		printf("%s[%s](%d)\t", networks[i].name, networks[i].symb, networks[i].fd);
	printf("\n");

	while (htiterate(users, &it)) {
		fds = (int *)it.node->val;
		/* print user nick */
		printf("%s\t", (char *)it.node->key);
		/* print clones fd separated by tabs */
		for (i = 0; i < netlen; i++) {
			printf("%d", fds[i]);
			if ((fds[i] > 0) && (fdsuf[fds[i]] > 0))
				printf("(%d)", fdsuf[fds[i]]);
			printf("\t");
		}
		printf("\n");
	}
}


void
print_users(void)
{
	Htiter	it = {0};
	int	index = -1;
	while (htiterate(users, &it)) {
		if (index != (int)it.index) {
			printf("\n%d", it.index);
			index = (int)it.index;
		}
		printf(" -> %s", (char *)it.node->key);
	}
	printf("\n");
}

int
fdadd(char *host, char *port)
{
	int fd;
	if ((fd = dial(host, port)) == -1)
		return -1;

	FD_SET(fd, &fdset);
	if (fd+1 > nfds)
		nfds = fd+1;

	return fd;
}

void
fddel(int fd, char *msg)
{
	ircsend(fd, "QUIT :%s", msg);
	close(fd);
	FD_CLR(fd, &fdset);
	fdnet[fd] = -1;
}

void
netadd(char *name, char *symb, char *host, char *port, char *chan)
{
	int i, fd;
	Network *n;
	Htiter	it = {0};

	/* resize */
	if (netlen < netcap) {
		netcap += NET_ADDEND;
		networks = realloc(networks, (size_t)netcap * sizeof(Network));

		while (htiterate(users, &it))
			it.node->val = realloc(it.node->val, (size_t)netcap * sizeof(int));
	}

	/* if name or symbol or host:port is already taken */
	for (i = 0; i < netlen; i++) {
		if (strcmp(networks[i].name, name) == 0) {
			printf("error: network name '%s' already taken\n", name);
			return;
		}
		if (strcmp(networks[i].symb, symb) == 0) {
			printf("error: network symbol '%s' already taken\n", symb);
			return;
		}
		/* if ((strcmp(networks[i].host, host) == 0)
		&& (strcmp(networks[i].port, port) == 0)) {
			printf("error: network host '%s' and port '%s' already taken by network '%s'\n", host, port, networks[i].name);
			return;
		}*/
	}

	printf("info: adding network '%s'\n", name);
	if ((fd = fdadd(host, port)) == -1)
		return;
	/* send NICK and USER commands */
	ircsend(fd, "NICK linker");
	ircsend(fd, "USER linker localhost %s :%s", host, name);
	/* add a network */
	fdnet[fd] = netlen;
	fdtype[fd] = FD_TYPE_LINKER;

	n = &networks[netlen];
	n->fd = fd;
	n->name = strdup(name);
	n->symb = strdup(symb);
	n->host = strdup(host);
	n->port = strdup(port);
	n->chan = strdup(chan);
	netlen++;
}

void
__netdel(int index)
{
	Network *n = &networks[index];
	free(n->name);
	free(n->symb);
	free(n->host);
	free(n->port);
	free(n->chan);
}

void
netdel(char *name)
{
	int i, netindex = -1, *fds;
	char quit_msg[BUFFER_LEN];

	Htiter	it	= {0};	/* current iterator */
	Htiter	lastit	= {0};	/* last iterator */

	/* check if the netname exists */
	for (i = 0; i < netlen; i++) {
		if (strcmp(name, networks[i].name) == 0) {
			netindex = i;
			break;
		}
	}
	if (netindex == -1) {
		printf("error: network name '%s' doesn't exist\n", name);
		return;
	}

	printf("info: deleting network '%s'\n", name);

	/* set the quit msg */
	snprintf(quit_msg, BUFFER_LEN, "unlinking network %s", name);

	/* set all fds with last network index to the current index. */
	for (i = 0; i < nfds; i++) {
		if (fdnet[i] == netlen-1)
			fdnet[i] = netindex;
	}

	while (htiterate(users, &it)) {
		fds = (int *)it.node->val;
		/* delete the user */
		if (fds[netindex] == -1) {
			userdel(it.node->key, quit_msg);
			/* this node is deleted and has no next */
			it = lastit;
		/* delete the clone */
		} else {
			if (fds[netindex] > 0)
				fddel(fds[netindex], quit_msg);

			/* swap last index with the netindex */
			fds[netindex] = fds[netlen-1];
			fds[netlen-1] = 0;
		}
		lastit = it;
	}

	fddel(networks[netindex].fd, quit_msg);
	 __netdel(netindex);

	/* swap the network with the last */
	networks[netindex] = networks[netlen-1];
	netlen--;
}

int
cloneadd(int netindex, char *nick)
{
	int fd;
	Network *n = &networks[netindex];

	printf("info: adding clone '%s' on network '%s'\n", nick, n->name);
	if ((fd = fdadd(n->host, n->port)) == -1)
		return -1;
	/* send NICK and USER commands */
	ircsend(fd, "NICK %s", nick);
	ircsend(fd, "USER username localhost %s :real name", n->host);
	/* add a user */
	fdnet[fd] = netindex;
	fdtype[fd] = FD_TYPE_CLONE;
	fdsuf[fd] = 0;
	return fd;
}

/* update clones in the last network added */
void
clonesupdate(void)
{
	int	*fds;
	Htiter	 it = {0};

	while (htiterate(users, &it)) {
		fds = (int *)it.node->val;
		/* netlen-1 can be occupied if networks are added quickly
		 * and has not enough time to connect to it's server */
		if (fds[netlen-1] == 0)
			fds[netlen-1] = cloneadd(netlen-1, it.node->key);
	}
}

void
useradd(char *unick, int nfd)
{
	int i, *fds;
	Network *n;
	size_t l;
	char *nick;

	n = &networks[fdnet[nfd]];

	/* too long name */
	l = strlen(unick) + strlen(n->symb) + 2 + 1;
	if (l > 16)
		return;

	nick = ecalloc(l, sizeof(char));
	fds = ecalloc((size_t)netcap, sizeof(int));
	sprintf(nick, "%s[%s]", unick, n->symb);

	/* resize */
	if ((users->cap - users->len) < USER_ADDEND)
		htresize(users, users->cap + USER_ADDEND);

	for (i = 0; i < netlen; i++) {
		/* set -1 on user network */
		if (i == fdnet[nfd]) {
			fds[i] = -1;
		} else {
			fds[i] = cloneadd(i, nick);
		}
	}

	if (htinsert(users, nick, fds) == -1)
		fatal("error: user '%s' already exist", nick);
}

void
userdel(char *nick, char *msg)
{
	int *fds, i;
	if ((fds = htsearch(users, nick)) == NULL)
		return;

	for (i = 0; i < netlen; i++) {
		if (fds[i] > 0)
			fddel(fds[i], msg);
	}
	htremove(users, nick);
}

void
append_netsymb(char *nick, int fd)
{
	strcat(nick, "[");
	strcat(nick, networks[fdnet[fd]].symb);
	strcat(nick, "]");
}

int *
getuserfds(char *nick, int nfd)
{
	unsigned int l;
	int *fds = NULL;

	for (l = 0; nick[strlen(nick)-l-1] == '_'; l++);
	if (l > 0)
		nick[strlen(nick)-l] = '\0';

	fds = htsearch(users, nick);
	/* if match but suffix doesn't match */
	if ((fds != NULL) && (fdsuf[fds[fdnet[nfd]]] != (int)l))
		fds = NULL;

	/* add the removed delimiter */
	if (l > 0)
		nick[strlen(nick)] = '_';

	return fds;
}

/* trim all the nicknames to original nick
 * this src will be destructed */
void
privmsg_update(char *dst, char *src, int fd)
{
	char d;		/* delimiter */
	char *n;

	while (src != NULL) {
		n = strpbrk(src, " :;,<>@&~%+\\");
		if (n == NULL) {
			d = '\0';
		} else {
			d = *n;
			*n = '\0';
			n++;
		}

		/* check if the word is nick */
		if (getuserfds(src, fd) != NULL)
			*strrchr(src, '[') = '\0';

		strcat(dst, src);
		strncat(dst, &d, 1);

		src = n;
	}
}

void
handle_server(int fd)
{
	char backup [BUFFER_LEN];
	char buffer [BUFFER_LEN];
	char *buf;
	char *cmd;
	char *nick;
	int i;

	char linker[BUFFER_LEN] = "linker";

	if (dreadline(fd, buffer, sizeof(buffer)) < 1)
		fatal("error: %d: remote host closed connection: %s", fd, strerror(errno));

	strcpy(backup, buffer);
	buf = buffer;

	/* remove CRLFs */
	for (i = 0; i < (int)strlen(buf); i++) {
		if (buf[i] == '\r' || buf[i] == '\n') {
			buf[i] = '\0';
			break;
		}
	}

	/* first column */
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "NOTICE") == 0) {
		return;
	} else if (strcmp(cmd, "ERROR") == 0) {
		goto printbuffer;
	} else if (strcmp(cmd, "PING") == 0) {
		ircsend(fd, "PONG %s", buf);
		return;
	}
	/* strip nick from first column */
	nick = split(&cmd, '!');
	if (nick[0] == ':')
		nick++;

	/* second column */
	cmd = split(&buf, ' ');
	/* these messages are handled by linker */
	if (fdtype[fd] == FD_TYPE_CLONE) {
		if ((strcmp(cmd, "353") == 0)
		|| (strcmp(cmd, "JOIN") == 0)
		|| (strcmp(cmd, "QUIT") == 0)
		|| (strcmp(cmd, "PART") == 0)
		|| (strcmp(cmd, "KICK") == 0)
		|| (strcmp(cmd, "NICK") == 0))
			return;
	}

	/* set linker nick */
	for (i = 0; i < fdsuf[fd]; i++)
		strcat(linker, "_");
	/* ignore all the info messages */
	if ((strcmp(cmd, "002") == 0)
	|| (strcmp(cmd, "003") == 0)
	|| (strcmp(cmd, "004") == 0)
	|| (strcmp(cmd, "005") == 0)
	|| (strcmp(cmd, "003") == 0)
	|| (strcmp(cmd, "251") == 0)
	|| (strcmp(cmd, "252") == 0)
	|| (strcmp(cmd, "254") == 0)
	|| (strcmp(cmd, "255") == 0)
	|| (strcmp(cmd, "265") == 0)
	|| (strcmp(cmd, "266") == 0)
	|| (strcmp(cmd, "250") == 0)
	|| (strcmp(cmd, "375") == 0)
	|| (strcmp(cmd, "372") == 0)
	|| (strcmp(cmd, "376") == 0)
	|| (strcmp(cmd, "396") == 0)
	|| (strcmp(cmd, "366") == 0)
	|| (strcmp(cmd, "MODE") == 0)
	|| (strcmp(cmd, "NOTICE") == 0)) {
		return;
	} else if (strcmp(cmd, "433") == 0) {	/* Nickname already in use */
		split(&buf, ' ');
		nick = split(&buf, ' ');
		strcat(nick, "_");
		fdsuf[fd]++;
		ircsend(fd, "NICK %s", nick);
		return;
	} else if (strcmp(cmd, "001") == 0) {
		ircsend(fd, "JOIN %s", networks[fdnet[fd]].chan);
		return;

	/* ========================= only linker ========================= */
	} else if (strcmp(cmd, "353") == 0) {
		char *nick;
		split(&buf, ':');
		clonesupdate();
		while (*(nick = split(&buf, ' ')) != '\0') {
			if (*nick == '@'
			|| *nick == '&'
			|| *nick == '~'
			|| *nick == '%'
			|| *nick == '+'
			|| *nick == '\\')
				nick++;
			if (strcmp(nick, linker) != 0)
				useradd(nick, fd);
		}
		return;
	} else if (strcmp(cmd, "JOIN") == 0) {
		if ((strcmp(nick, linker) != 0)
		&& (getuserfds(nick, fd) == NULL))	/* if not clone */
			useradd(nick, fd);
		return;
	} else if ((strcmp(cmd, "QUIT") == 0)
		|| (strcmp(cmd, "PART") == 0)) {
		append_netsymb(nick, fd);
		userdel(nick, buf);
		return;
	} else if (strcmp(cmd, "NICK") == 0) {
		int *fds, i;
		char *newnick;

		append_netsymb(nick, fd);
		if ((fds = htsearch(users, nick)) == NULL)
			return;

		/* set buf to new nick */
		split(&buf, ':');
		/* allocate a newnick and append the netsym and then replace the old */
		newnick = ecalloc(strlen(buf) + strlen(networks[fdnet[fd]].symb) + 2 + 1, sizeof(char));
		sprintf(newnick, "%s[%s]", buf, networks[fdnet[fd]].symb);
		htsetkey(users, nick, newnick);

		for (i = 0; i < netlen; i++) {
			if (fds[i] > 0)
				ircsend(fds[i], "NICK %s", newnick);
		}
		return;
	} else if (strcmp(cmd, "KICK") == 0) {
		/* :<nick_which_is_kicking>!~user@host KICK <channel> <nick_which_has_been_kicked> :<kick_msg> */
		int *fds;
		char *chan = split(&buf, ' ');
		char *user = split(&buf, ' ');
		char quit_msg[BUFFER_LEN];

		/* set the quit msg */
		snprintf(quit_msg, BUFFER_LEN, "kicked by %s", nick);

		if (strcmp(user, linker) == 0) {
			netdel(networks[fdnet[fd]].name);
			return;
		}

		/* delete the user if the message from the same network */
		if ((fds = getuserfds(user, fd)) == NULL) {
			append_netsymb(user, fd);
			userdel(user, quit_msg);
			return;
		}

		/* remove netsymb and suffix */
		*strrchr(user, '[') = '\0';

		/* get the original user fd index */
		for (i = 0; i < netlen; i++) {
			if (fds[i] == -1)
				break;
		}

		/* set buf to msg */
		split(&buf, ':');	
		/* send notice in the channel through linker */
		ircsend(networks[i].fd, "PRIVMSG %s :%s was kicked out by %s from network %s %s [%s]",
				networks[i].chan, user, nick, networks[fdnet[fd]].name, chan, buf);
		
		/* close the kicked fd */
		fddel(fds[fdnet[fd]], quit_msg);
		fds[fdnet[fd]] = -2;
		return;
	/* ========================= only linker ========================= */

	} else if (strcmp(cmd, "PRIVMSG") == 0) {
		char msg[BUFFER_LEN] = "";
		int *fds;

		if (fdtype[fd] == FD_TYPE_LINKER) {
			append_netsymb(nick, fd);
			if ((fds = htsearch(users, nick)) == NULL)
				return;

			split(&buf, ':');	/* set buf to msg */
			privmsg_update(msg, buf, fd);
			for (i = 0; i < netlen; i++) {
				if (fds[i] > 0)
					ircsend(fds[i], "PRIVMSG %s :%s", networks[i].chan, msg);
			}
		} else {
			char *netsymb;
			char *user = split(&buf, ' ');

			/* ignore messages from channel (it is handled by linker) */
			if ((user[0] == '#') || (user[0] == '&'))
				return;

			append_netsymb(nick, fd);
			if ((fds = htsearch(users, nick)) == NULL)
				return;

			/* split user nick and network symbol */
			*strrchr(user, ']') = '\0';
			netsymb = strrchr(user, '[');
			*netsymb++ = '\0';

			/* get the network index */
			for (i = 0; i < netlen; i++) {
				if (strcmp(netsymb, networks[i].symb) == 0)
					break;
			}

			split(&buf, ':');	/* set buf to msg */
			privmsg_update(msg, buf, fd);
			ircsend(fds[i], "PRIVMSG %s :%s", user, msg);
		}
		return;
	}

printbuffer:
	printf("%d: %s\n", fd, backup);
}

void
handle_fifo(int fd)
{
	char buffer[BUFFER_LEN];
	char *buf;
	char *cmd;

	if (dreadline(fd, buffer, sizeof(buffer)) == -1)
		fatal("error: dreadline() failed");

	/* printf("fifo: %s\n", buffer); */

	buf = buffer;
	cmd = split(&buf, ' ');
	if (strcmp(cmd, "netadd") == 0) {
		char *name = split(&buf, ' ');
		char *symb = split(&buf, ' ');
		char *host = split(&buf, ' ');
		char *port = split(&buf, ' ');
		char *chan = buf;
		if ((*name == '\0') ||  (*symb == '\0') ||  (*host == '\0') ||  (*port == '\0') || (*chan == '\0'))
			printf("usage: netadd <name> <symbol> <hostname> <port> <channel>\n");
		netadd(name, symb, host, port, chan);
	} else if (strcmp(cmd, "netdel") == 0) {
		char *name = buf;
		if (*name == '\0')
			printf("usage: netdel <name>\n");
		netdel(name);
	} else if (strcmp(cmd, "print") == 0) {
		print_table();
	} else if (strcmp(cmd, "users") == 0) {
		print_users();
	} else if (strcmp(cmd, "exit") == 0) {
		isrunning = 0;
	} else {
		printf("error: %s is not a command\n", cmd);
	}
}

int
main(int argc, char *argv[])
{
	int		fifofd, r, i;
	fd_set		rdset;
	struct stat	fifo_st;

	/* check arguments */
	if (argc != 2)
		fatal("usage: %s <fifo>", argv[0]);

	/* warning */
	printf("WARNING: This software is provided 'as-is', without any express or "
		"implied warranty.\nIn no event shall the author(s) be liable "
		"for any damages arising from the use of this software.\n");

	/* init networks and users */
	networks = ecalloc((size_t)netcap, sizeof(Network));
	users = htcreate((KeyLenFn *)strlen, (KeyCmpFn *)strcmp, free, free, USER_ADDEND);
	/* init fdnet array to -1 */
	for (i = 0; i < MAX_FD; i++)
		fdnet[i] = -1;

	/* crate or/and open fifo */
	if (stat(argv[1], &fifo_st) == 0) {
		if (!(fifo_st.st_mode & S_IFIFO))
			fatal("error: file %s is not a fifo", argv[1]);
	} else if (mkfifo(argv[1], S_IRWXU) != 0) {
		fatal("error: cannot create fifo file %s", argv[1]);
	}
	fifofd = open(argv[1], O_RDWR);
	if (fifofd < 0)
		fatal("error: cannot open() '%s'", argv[1]);

	/* set stdout to unbufferd */
	setvbuf(stdout, NULL, _IONBF, 0);
	/* initialize fdset */
	FD_ZERO(&fdset);
	FD_SET(fifofd, &fdset);
	nfds = fifofd + 1;

	/* select loop */
	while (isrunning) {
		rdset = fdset;
		r = select(nfds, &rdset, 0, 0, NULL);
		if (r < 0) {
			if (errno == EINTR)
				continue;
			fatal("error: select: %s", strerror(errno));
		} else if (r == 0) {
			fatal("error: select() timeout");
		}

		for (i = 0; i < nfds; i++) {
			if (FD_ISSET(i, &rdset)) {
				if (i == fifofd)
					handle_fifo(i);
				else
					handle_server(i);

				/* handle one fd at one time because FD_CLR */
				break;
			}
		}
	}

	for (i = 0; i < nfds; i++) {
		if (fdnet[i] > 0) {
			printf("shutting down %d\n", i);
			fddel(i, "linker shutting down");
		}
	}
	/* delete all the users */
	htdestroy(users);

	/* delete all the networks */
	for (i = 0; i < netlen; i++)
		__netdel(i);

	free(networks);
	return 0;
}