THW patch applied
[iii.git] / ii.c
blobee72812b659b0761904f14b4f41d6034b5977160
1 /*
2 * (C)opyright MMV-MMVI Anselm R. Garbe <garbeam at gmail dot com>
3 * (C)opyright MMV-MMVII Nico Golde <nico at ngolde dot de>
4 * See LICENSE file for license details.
5 */
6 #include <errno.h>
7 #include <netdb.h>
8 #include <netinet/in.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <limits.h>
12 #include <fcntl.h>
13 #include <string.h>
14 #include <signal.h>
15 #include <sys/stat.h>
16 #include <sys/socket.h>
17 #include <ctype.h>
18 #include <time.h>
19 #include <unistd.h>
21 #ifndef PIPE_BUF /* FreeBSD don't know PIPE_BUF */
22 #define PIPE_BUF 4096
23 #endif
25 enum { TOK_NICKSRV = 0, TOK_USER, TOK_CMD, TOK_CHAN, TOK_ARG, TOK_TEXT, TOK_LAST };
27 typedef struct Channel Channel;
28 struct Channel {
29 int fd;
30 char *name;
31 Channel *next;
34 #define PING_TIMEOUT 300
35 #define SERVER_PORT 6667
36 static int irc;
37 static time_t last_response;
38 static Channel *channels = NULL;
39 static char *host = "irc.freenode.net";
40 static char nick[32] = "anonymous"; /* might change while running */
41 static char path[_POSIX_PATH_MAX];
42 static char message[PIPE_BUF]; /* message buf used for communication */
44 static void usage() {
45 fprintf(stderr, "%s",
46 "ii - irc it - " VERSION "\n"
47 "(C)opyright MMV-MMVI Anselm R. Garbe\n"
48 "(C)opyright MMV-MMVII Nico Golde\n"
49 "usage: ii [-i <irc dir>] [-s <host>] [-p <port>]\n"
50 " [-n <nick>] [-k <password>] [-f <fullname>]\n");
51 exit(EXIT_SUCCESS);
53 static char *striplower(char *s) {
54 char *p = NULL;
55 for(p = s; p && *p; p++) {
56 if(*p == '/') *p = '_';
57 *p = tolower(*p);
59 return s;
62 /* creates directories top-down, if necessary */
63 static void create_dirtree(const char *dir) {
64 char tmp[256];
65 char *p = NULL;
66 size_t len;
68 snprintf(tmp, sizeof(tmp),"%s",dir);
69 len = strlen(tmp);
70 if(tmp[len - 1] == '/')
71 tmp[len - 1] = 0;
72 for(p = tmp + 1; *p; p++)
73 if(*p == '/') {
74 *p = 0;
75 mkdir(tmp, S_IRWXU);
76 *p = '/';
78 mkdir(tmp, S_IRWXU);
81 static int get_filepath(char *filepath, size_t len, char *channel, char *file) {
82 if(channel) {
83 if(!snprintf(filepath, len, "%s/%s", path, striplower(channel)))
84 return 0;
85 create_dirtree(filepath);
86 return snprintf(filepath, len, "%s/%s/%s", path, striplower(channel), file);
88 return snprintf(filepath, len, "%s/%s", path, file);
91 static void create_filepath(char *filepath, size_t len, char *channel, char *suffix) {
92 if(!get_filepath(filepath, len, channel, suffix)) {
93 fprintf(stderr, "%s", "ii: path to irc directory too long\n");
94 exit(EXIT_FAILURE);
98 static int open_channel(char *name) {
99 static char infile[256];
100 create_filepath(infile, sizeof(infile), name, "in");
101 if(access(infile, F_OK) == -1)
102 mkfifo(infile, S_IRWXU);
103 return open(infile, O_RDONLY | O_NONBLOCK, 0);
106 static void add_channel(char *name) {
107 Channel *c;
108 int fd;
110 for(c = channels; c; c = c->next)
111 if(!strcmp(name, c->name))
112 return; /* already handled */
114 fd = open_channel(name);
115 if(fd == -1) {
116 printf("ii: exiting, cannot create in channel: %s\n", name);
117 exit(EXIT_FAILURE);
119 c = calloc(1, sizeof(Channel));
120 if(!c) {
121 perror("ii: cannot allocate memory");
122 exit(EXIT_FAILURE);
124 if(!channels) channels = c;
125 else {
126 c->next = channels;
127 channels = c;
129 c->fd = fd;
130 c->name = strdup(name);
133 static void rm_channel(Channel *c) {
134 Channel *p;
135 if(channels == c) channels = channels->next;
136 else {
137 for(p = channels; p && p->next != c; p = p->next);
138 if(p->next == c)
139 p->next = c->next;
141 free(c->name);
142 free(c);
145 static void login(char *key, char *fullname) {
146 if(key) snprintf(message, PIPE_BUF,
147 "PASS %s\r\nNICK %s\r\nUSER %s localhost * :%s\r\n", key,
148 nick, nick, fullname ? fullname : nick);
149 else snprintf(message, PIPE_BUF, "NICK %s\r\nUSER %s localhost * :%s\r\n",
150 nick, nick, fullname ? fullname : nick);
152 write(irc, message, strlen(message)); /* login */
155 static int tcpopen(unsigned short port) {
156 int fd;
157 struct sockaddr_in sin;
158 struct hostent *hp = gethostbyname(host);
160 memset(&sin, 0, sizeof(struct sockaddr_in));
161 if(!hp) {
162 perror("ii: cannot retrieve host information");
163 exit(EXIT_FAILURE);
165 sin.sin_family = AF_INET;
166 memcpy(&sin.sin_addr, hp->h_addr, hp->h_length);
167 sin.sin_port = htons(port);
168 if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
169 perror("ii: cannot create socket");
170 exit(EXIT_FAILURE);
172 if(connect(fd, (const struct sockaddr *) &sin, sizeof(sin)) < 0) {
173 perror("ii: cannot connect to host");
174 exit(EXIT_FAILURE);
176 return fd;
179 static size_t tokenize(char **result, size_t reslen, char *str, char delim) {
180 char *p = NULL, *n = NULL;
181 size_t i;
183 if(!str)
184 return 0;
185 for(n = str; *n == ' '; n++);
186 p = n;
187 for(i = 0; *n != 0;) {
188 if(i == reslen)
189 return 0;
190 if(i > TOK_CHAN - TOK_CMD && strtol(result[0], NULL, 10) > 0) delim=':'; /* workaround non-RFC compliant messages */
191 if(*n == delim) {
192 *n = 0;
193 result[i++] = p;
194 p = ++n;
195 } else
196 n++;
198 if(i<reslen && p < n && strlen(p))
199 result[i++] = p;
200 return i; /* number of tokens */
203 static void print_out(char *channel, char *buf) {
204 static char outfile[256], server[256], buft[18];
205 FILE *out = NULL;
206 time_t t = time(0);
208 if(channel) snprintf(server, sizeof(server), "-!- %s", channel);
209 if(strstr(buf, server)) channel="";
210 create_filepath(outfile, sizeof(outfile), channel, "out");
211 if(!(out = fopen(outfile, "a"))) return;
213 strftime(buft, sizeof(buft), "%F %R", localtime(&t));
214 fprintf(out, "%s %s\n", buft, buf);
215 fclose(out);
218 static void proc_channels_privmsg(char *channel, char *buf) {
219 snprintf(message, PIPE_BUF, "<%s> %s", nick, buf);
220 print_out(channel, message);
221 snprintf(message, PIPE_BUF, "PRIVMSG %s :%s\r\n", channel, buf);
222 write(irc, message, strlen(message));
225 static void proc_channels_input(Channel *c, char *buf) {
226 static char infile[256];
227 char *p = NULL;
229 if(buf[0] != '/' && buf[0] != 0) {
230 proc_channels_privmsg(c->name, buf);
231 return;
233 message[0] = '\0';
234 switch (buf[1]) {
235 case 'j':
236 p = strchr(&buf[3], ' ');
237 if(p) *p = 0;
238 if((buf[3]=='#')||(buf[3]=='&')||(buf[3]=='+')||(buf[3]=='!')){
239 if(p) snprintf(message, PIPE_BUF, "JOIN %s %s\r\n", &buf[3], p + 1); /* password protected channel */
240 else snprintf(message, PIPE_BUF, "JOIN %s\r\n", &buf[3]);
241 add_channel(&buf[3]);
243 else {
244 if(p){
245 add_channel(&buf[3]);
246 proc_channels_privmsg(&buf[3], p + 1);
247 return;
250 break;
251 case 't':
252 if(strlen(buf)>=3) snprintf(message, PIPE_BUF, "TOPIC %s :%s\r\n", c->name, &buf[3]);
253 break;
254 case 'a':
255 if(strlen(buf)>=3){
256 snprintf(message, PIPE_BUF, "-!- %s is away \"%s\"", nick, &buf[3]);
257 print_out(c->name, message);
259 if(buf[2] == 0 || strlen(buf)<3) /* or used to make else part safe */
260 snprintf(message, PIPE_BUF, "AWAY\r\n");
261 else
262 snprintf(message, PIPE_BUF, "AWAY :%s\r\n", &buf[3]);
263 break;
264 case 'n':
265 if(strlen(buf)>=3){
266 snprintf(nick, sizeof(nick),"%s", &buf[3]);
267 snprintf(message, PIPE_BUF, "NICK %s\r\n", &buf[3]);
269 break;
270 case 'l':
271 if(c->name[0] == 0)
272 return;
273 if(buf[2] == ' ' && strlen(buf)>=3)
274 snprintf(message, PIPE_BUF, "PART %s :%s\r\n", c->name, &buf[3]);
275 else
276 snprintf(message, PIPE_BUF,
277 "PART %s\r\n", c->name);
278 write(irc, message, strlen(message));
279 close(c->fd);
280 create_filepath(infile, sizeof(infile), c->name, "in");
281 unlink(infile);
282 rm_channel(c);
283 return;
284 break;
285 default:
286 snprintf(message, PIPE_BUF, "%s\r\n", &buf[1]);
287 break;
289 if (message[0] != '\0')
290 write(irc, message, strlen(message));
293 static void proc_server_cmd(char *buf) {
294 char *argv[TOK_LAST], *cmd = NULL, *p = NULL;
295 int i;
297 if(!buf || *buf=='\0')
298 return;
300 for(i = 0; i < TOK_LAST; i++)
301 argv[i] = NULL;
302 /* <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
303 <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
304 <command> ::= <letter> { <letter> } | <number> <number> <number>
305 <SPACE> ::= ' ' { ' ' }
306 <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
307 <middle> ::= <Any *non-empty* sequence of octets not including SPACE
308 or NUL or CR or LF, the first of which may not be ':'>
309 <trailing> ::= <Any, possibly *empty*, sequence of octets not including NUL or CR or LF>
310 <crlf> ::= CR LF */
312 if(buf[0] == ':') { /* check prefix */
313 if (!(p = strchr(buf, ' '))) return;
314 *p = 0;
315 for(++p; *p == ' '; p++);
316 cmd = p;
317 argv[TOK_NICKSRV] = &buf[1];
318 if((p = strchr(buf, '!'))) {
319 *p = 0;
320 argv[TOK_USER] = ++p;
322 } else
323 cmd = buf;
325 /* remove CRLFs */
326 for(p = cmd; p && *p != 0; p++)
327 if(*p == '\r' || *p == '\n')
328 *p = 0;
330 if((p = strchr(cmd, ':'))) {
331 *p = 0;
332 argv[TOK_TEXT] = ++p;
335 tokenize(&argv[TOK_CMD], TOK_LAST - TOK_CMD, cmd, ' ');
337 if(!argv[TOK_CMD] || !strncmp("PONG", argv[TOK_CMD], 5)) {
338 return;
339 } else if(!strncmp("PING", argv[TOK_CMD], 5)) {
340 snprintf(message, PIPE_BUF, "PONG %s\r\n", argv[TOK_TEXT]);
341 write(irc, message, strlen(message));
342 return;
343 } else if(!argv[TOK_NICKSRV] || !argv[TOK_USER]) { /* server command */
344 snprintf(message, PIPE_BUF, "%s%s", argv[TOK_ARG] ? argv[TOK_ARG] : "", argv[TOK_TEXT] ? argv[TOK_TEXT] : "");
345 print_out(0, message);
346 return;
347 } else if(!strncmp("ERROR", argv[TOK_CMD], 6))
348 snprintf(message, PIPE_BUF, "-!- error %s", argv[TOK_TEXT] ? argv[TOK_TEXT] : "unknown");
349 else if(!strncmp("JOIN", argv[TOK_CMD], 5)) {
350 if(argv[TOK_TEXT] != NULL){
351 p = strchr(argv[TOK_TEXT], ' ');
352 if(p)
353 *p = 0;
355 argv[TOK_CHAN] = argv[TOK_TEXT];
356 snprintf(message, PIPE_BUF, "-!- %s(%s) has joined %s", argv[TOK_NICKSRV], argv[TOK_USER], argv[TOK_TEXT]);
357 } else if(!strncmp("PART", argv[TOK_CMD], 5)) {
358 snprintf(message, PIPE_BUF, "-!- %s(%s) has left %s", argv[TOK_NICKSRV], argv[TOK_USER], argv[TOK_CHAN]);
359 } else if(!strncmp("MODE", argv[TOK_CMD], 5))
360 snprintf(message, PIPE_BUF, "-!- %s changed mode/%s -> %s %s", argv[TOK_NICKSRV], argv[TOK_CMD + 1] ? argv[TOK_CMD + 1] : "" , argv[TOK_CMD + 2]? argv[TOK_CMD + 2] : "", argv[TOK_CMD + 3] ? argv[TOK_CMD + 3] : "");
361 else if(!strncmp("QUIT", argv[TOK_CMD], 5))
362 snprintf(message, PIPE_BUF, "-!- %s(%s) has quit \"%s\"", argv[TOK_NICKSRV], argv[TOK_USER], argv[TOK_TEXT] ? argv[TOK_TEXT] : "");
363 else if(!strncmp("NICK", argv[TOK_CMD], 5))
364 snprintf(message, PIPE_BUF, "-!- %s changed nick to %s", argv[TOK_NICKSRV], argv[TOK_TEXT]);
365 else if(!strncmp("TOPIC", argv[TOK_CMD], 6))
366 snprintf(message, PIPE_BUF, "-!- %s changed topic to \"%s\"", argv[TOK_NICKSRV], argv[TOK_TEXT] ? argv[TOK_TEXT] : "");
367 else if(!strncmp("KICK", argv[TOK_CMD], 5))
368 snprintf(message, PIPE_BUF, "-!- %s kicked %s (\"%s\")", argv[TOK_NICKSRV], argv[TOK_ARG], argv[TOK_TEXT] ? argv[TOK_TEXT] : "");
369 else if(!strncmp("NOTICE", argv[TOK_CMD], 7))
370 snprintf(message, PIPE_BUF, "-!- \"%s\")", argv[TOK_TEXT] ? argv[TOK_TEXT] : "");
371 else if(!strncmp("PRIVMSG", argv[TOK_CMD], 8))
372 snprintf(message, PIPE_BUF, "<%s> %s", argv[TOK_NICKSRV], argv[TOK_TEXT] ? argv[TOK_TEXT] : "");
373 if(!argv[TOK_CHAN] || !strncmp(argv[TOK_CHAN], nick, strlen(nick)))
374 print_out(argv[TOK_NICKSRV], message);
375 else
376 print_out(argv[TOK_CHAN], message);
379 static int read_line(int fd, size_t res_len, char *buf) {
380 size_t i = 0;
381 char c = 0;
382 do {
383 if(read(fd, &c, sizeof(char)) != sizeof(char))
384 return -1;
385 buf[i++] = c;
387 while(c != '\n' && i < res_len);
388 buf[i - 1] = 0; /* eliminates '\n' */
389 return 0;
392 static void handle_channels_input(Channel *c) {
393 static char buf[PIPE_BUF];
394 if(read_line(c->fd, PIPE_BUF, buf) == -1) {
395 close(c->fd);
396 int fd = open_channel(c->name);
397 if(fd != -1)
398 c->fd = fd;
399 else
400 rm_channel(c);
401 return;
403 proc_channels_input(c, buf);
406 static void handle_server_output() {
407 static char buf[PIPE_BUF];
408 if(read_line(irc, PIPE_BUF, buf) == -1) {
409 perror("ii: remote host closed connection");
410 exit(EXIT_FAILURE);
412 proc_server_cmd(buf);
415 static void run() {
416 Channel *c;
417 int r, maxfd;
418 fd_set rd;
419 struct timeval tv;
420 char ping_msg[17] = "PING localhost\r\n";
422 for(;;) {
423 FD_ZERO(&rd);
424 maxfd = irc;
425 FD_SET(irc, &rd);
426 for(c = channels; c; c = c->next) {
427 if(maxfd < c->fd)
428 maxfd = c->fd;
429 FD_SET(c->fd, &rd);
432 tv.tv_sec = 120;
433 tv.tv_usec = 0;
434 r = select(maxfd + 1, &rd, 0, 0, &tv);
435 if(r < 0) {
436 if(errno == EINTR)
437 continue;
438 perror("ii: error on select()");
439 exit(EXIT_FAILURE);
440 } else if(r == 0) {
441 if(time(NULL) - last_response >= PING_TIMEOUT) {
442 print_out(NULL, "-!- ii shutting down: ping timeout");
443 exit(EXIT_FAILURE);
445 write(irc, ping_msg, strlen(ping_msg));
446 continue;
448 if(FD_ISSET(irc, &rd)) {
449 handle_server_output();
450 last_response = time(NULL);
452 for(c = channels; c; c = c->next)
453 if(FD_ISSET(c->fd, &rd))
454 handle_channels_input(c);
458 int main(int argc, char *argv[]) {
459 int i;
460 unsigned short port = SERVER_PORT;
461 char *key = NULL, *fullname = NULL;
462 char prefix[_POSIX_PATH_MAX] = "irc";
464 if (argc <= 1 || (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'h')) usage();
466 for(i = 1; (i + 1 < argc) && (argv[i][0] == '-'); i++) {
467 switch (argv[i][1]) {
468 case 'i': snprintf(prefix,sizeof(prefix),"%s", argv[++i]); break;
469 case 's': host = argv[++i]; break;
470 case 'p': port = strtol(argv[++i], NULL, 10); break;
471 case 'n': snprintf(nick,sizeof(nick),"%s", argv[++i]); break;
472 case 'k': key = argv[++i]; break;
473 case 'f': fullname = argv[++i]; break;
474 default: usage(); break;
477 irc = tcpopen(port);
478 if(!snprintf(path, sizeof(path), "%s/%s", prefix, host)) {
479 fprintf(stderr, "%s", "ii: path to irc directory too long\n");
480 exit(EXIT_FAILURE);
482 create_dirtree(path);
484 add_channel(""); /* master channel */
485 login(key, fullname);
486 run();
488 return 0;