1 /* (C)opyright MMV-MMVI Anselm R. Garbe <garbeam at gmail dot com>
2 * (C)opyright MMV-MMXI Nico Golde <nico at ngolde dot de>
3 * See LICENSE file for license details. */
6 #include <netinet/in.h>
14 #include <sys/socket.h>
19 #include <openssl/rand.h>
20 #include <openssl/ssl.h>
21 #include <openssl/err.h>
24 #ifndef PIPE_BUF /* FreeBSD don't know PIPE_BUF */
27 #define PING_TIMEOUT 300
28 #define SERVER_PORT 6667
29 #define IS_CHANNEL(s) (((s)[0]=='#')||((s)[0]=='&')||((s)[0]=='+')||((s)[0]=='!'))
31 #define SSL_SERVER_PORT 6697
32 #define WRITE(conn, msg) (use_ssl ? SSL_write(irc->sslHandle, msg, strlen(msg)) : write(conn->irc, msg, strlen(msg)))
33 #define READ(fd, buf, s) (from_srv && use_ssl ? SSL_read(irc->sslHandle, buf, s) : read(fd, buf, s))
35 #define WRITE(irc, msg) (write(irc, msg, strlen(msg)))
36 #define READ(fd, buf, s) (read(fd, buf, s))
38 enum { TOK_NICKSRV
= 0, TOK_USER
, TOK_CMD
, TOK_CHAN
, TOK_ARG
, TOK_TEXT
, TOK_LAST
};
40 typedef struct Channel Channel
;
55 static size_t use_ssl
= 0;
57 static unsigned char fp
[EVP_MAX_MD_SIZE
];
62 static time_t last_response
;
63 static Channel
*channels
= NULL
;
64 static char *host
= "irc.freenode.net";
65 static char nick
[32] = "anonymous"; /* might change while running */
66 static char path
[_POSIX_PATH_MAX
];
67 static char message
[PIPE_BUF
]; /* message buf used for communication */
71 "ii - irc it - " VERSION
"\n"
72 "(C)opyright MMV-MMVI Anselm R. Garbe\n"
73 "(C)opyright MMV-MMXI Nico Golde\n"
74 "usage: ii [-i <irc dir>] [-s <host>] [-p <port>]\n"
75 " [-n <nick>] [-k <password>] [-f <fullname>]\n"
77 " [-e] [-d <directory>]\n");
79 " [-d <directory>]\n");
84 static char *striplower(char *s
) {
86 for(p
= s
; p
&& *p
; p
++) {
87 if(*p
== '/') *p
= '_';
93 /* creates directories top-down, if necessary */
94 static void create_dirtree(const char *dir
) {
99 snprintf(tmp
, sizeof(tmp
),"%s",dir
);
101 if(tmp
[len
- 1] == '/')
103 for(p
= tmp
+ 1; *p
; p
++)
112 static int get_filepath(char *filepath
, size_t len
, char *channel
, char *file
) {
114 if(!snprintf(filepath
, len
, "%s/%s", path
, channel
))
116 create_dirtree(filepath
);
117 return snprintf(filepath
, len
, "%s/%s/%s", path
, channel
, file
);
119 return snprintf(filepath
, len
, "%s/%s", path
, file
);
122 static void create_filepath(char *filepath
, size_t len
, char *channel
, char *suffix
) {
123 if(!get_filepath(filepath
, len
, striplower(channel
), suffix
)) {
124 fprintf(stderr
, "%s", "ii: path to irc directory too long\n");
129 static int open_channel(char *name
) {
130 static char infile
[256];
131 create_filepath(infile
, sizeof(infile
), name
, "in");
132 if(access(infile
, F_OK
) == -1)
133 mkfifo(infile
, S_IRWXU
);
134 return open(infile
, O_RDONLY
| O_NONBLOCK
, 0);
137 static void add_channel(char *cname
) {
140 char *name
= striplower(cname
);
142 for(c
= channels
; c
; c
= c
->next
)
143 if(!strcmp(name
, c
->name
))
144 return; /* already handled */
146 fd
= open_channel(name
);
148 printf("ii: exiting, cannot create in channel: %s\n", name
);
151 c
= calloc(1, sizeof(Channel
));
153 perror("ii: cannot allocate memory");
156 if(!channels
) channels
= c
;
162 c
->name
= strdup(name
);
165 static void rm_channel(Channel
*c
) {
167 if(channels
== c
) channels
= channels
->next
;
169 for(p
= channels
; p
&& p
->next
!= c
; p
= p
->next
);
177 static void login(char *key
, char *fullname
) {
178 if(key
) snprintf(message
, PIPE_BUF
,
179 "PASS %s\r\nNICK %s\r\nUSER %s localhost * :%s\r\n", key
,
180 nick
, nick
, fullname
? fullname
: nick
);
181 else snprintf(message
, PIPE_BUF
, "NICK %s\r\nUSER %s localhost * :%s\r\n",
182 nick
, nick
, fullname
? fullname
: nick
);
183 WRITE(irc
, message
); /* login */
187 static conn
*ssl_connect(int fd
) {
190 c
= malloc(sizeof(conn
));
192 perror("ii: cannot allocate memory");
199 c
->sslContext
= NULL
;
200 SSL_load_error_strings();
202 c
->sslContext
= SSL_CTX_new(SSLv23_client_method());
203 if(c
->sslContext
== NULL
)
204 ERR_print_errors_fp(stderr
);
205 c
->sslHandle
= SSL_new(c
->sslContext
);
206 if(!SSL_set_fd(c
->sslHandle
, c
->irc
) || SSL_connect(c
->sslHandle
) != 1)
207 ERR_print_errors_fp(stderr
);
208 if(!X509_digest(SSL_get_peer_certificate(c
->sslHandle
), EVP_md5(), fp
, &fp_len
))
209 ERR_print_errors_fp(stderr
);
215 static conn
*tcpopen(unsigned short port
) {
217 static int tcpopen(unsigned short port
) {
220 struct sockaddr_in sin
;
221 struct hostent
*hp
= gethostbyname(host
);
223 memset(&sin
, 0, sizeof(struct sockaddr_in
));
225 perror("ii: cannot retrieve host information");
228 sin
.sin_family
= AF_INET
;
229 memcpy(&sin
.sin_addr
, hp
->h_addr
, hp
->h_length
);
230 sin
.sin_port
= htons(port
);
231 if((fd
= socket(AF_INET
, SOCK_STREAM
, 0)) < 0) {
232 perror("ii: cannot create socket");
235 if(connect(fd
, (const struct sockaddr
*) &sin
, sizeof(sin
)) < 0) {
236 perror("ii: cannot connect to host");
241 return ssl_connect(fd
);
247 static size_t tokenize(char **result
, size_t reslen
, char *str
, char delim
) {
248 char *p
= NULL
, *n
= NULL
;
253 for(n
= str
; *n
== ' '; n
++);
255 for(i
= 0; *n
!= 0;) {
258 if(i
> TOK_CHAN
- TOK_CMD
&& strtol(result
[0], NULL
, 10) > 0) delim
=':'; /* workaround non-RFC compliant messages */
266 if(i
<reslen
&& p
< n
&& strlen(p
))
268 return i
; /* number of tokens */
271 static void print_out(char *channel
, char *buf
) {
272 static char outfile
[256], server
[256], buft
[18];
276 if(channel
) snprintf(server
, sizeof(server
), "-!- %s", channel
);
277 if(strstr(buf
, server
)) channel
="";
278 create_filepath(outfile
, sizeof(outfile
), channel
, "out");
279 if(!(out
= fopen(outfile
, "a"))) return;
280 if(channel
&& channel
[0]) add_channel(channel
);
282 strftime(buft
, sizeof(buft
), "%F %R", localtime(&t
));
283 fprintf(out
, "%s %s\n", buft
, buf
);
287 static void proc_channels_privmsg(char *channel
, char *buf
) {
288 snprintf(message
, PIPE_BUF
, "<%s> %s", nick
, buf
);
289 print_out(channel
, message
);
290 snprintf(message
, PIPE_BUF
, "PRIVMSG %s :%s\r\n", channel
, buf
);
294 static void proc_channels_input(Channel
*c
, char *buf
) {
297 if(buf
[0] && buf
[0] != '/') {
298 proc_channels_privmsg(c
->name
, buf
);
302 if(buf
[2] == ' ' || buf
[2] == '\0') switch (buf
[1]) {
304 if(buf
[3] == ' ' || buf
[3] == '\0') return;
305 p
= strchr(&buf
[3], ' ');
307 if(IS_CHANNEL(&buf
[3])){
308 if(p
&& strlen(p
+ 1)) snprintf(message
, PIPE_BUF
, "JOIN %s %s\r\n", &buf
[3], p
+ 1); /* password protected channel */
309 else snprintf(message
, PIPE_BUF
, "JOIN %s\r\n", &buf
[3]);
310 add_channel(&buf
[3]);
312 else if(p
&& strlen(p
+ 1)) {
313 add_channel(&buf
[3]);
314 proc_channels_privmsg(&buf
[3], p
+ 1);
319 if(IS_CHANNEL(&buf
[3])){
320 p
= strchr(&buf
[3], ' ');
322 if(p
&& strlen(p
+ 1)) snprintf(message
, PIPE_BUF
, "TOPIC %s :%s\r\n", &buf
[3], p
+ 1);
323 else snprintf(message
, PIPE_BUF
, "TOPIC %s\r\n", &buf
[3]);
326 if(c
->name
[0] == 0) return;
327 if(strlen(buf
)>3) snprintf(message
, PIPE_BUF
, "TOPIC %s :%s\r\n", c
->name
, &buf
[3]);
328 else snprintf(message
, PIPE_BUF
, "TOPIC %s\r\n", c
->name
);
333 snprintf(message
, PIPE_BUF
, "-!- %s is away \"%s\"", nick
, &buf
[3]);
334 print_out(c
->name
, message
);
335 snprintf(message
, PIPE_BUF
, "AWAY :%s\r\n", &buf
[3]);
336 } else snprintf(message
, PIPE_BUF
, "AWAY\r\n");
340 snprintf(nick
, sizeof(nick
),"%s", &buf
[3]);
341 snprintf(message
, PIPE_BUF
, "NICK %s\r\n", &buf
[3]);
345 if(c
->name
[0] == 0) return;
346 if(strlen(buf
)>3) snprintf(message
, PIPE_BUF
, "PART %s :%s\r\n", c
->name
, &buf
[3]);
347 else snprintf(message
, PIPE_BUF
, "PART %s\r\n", c
->name
);
348 if(IS_CHANNEL(c
->name
)) WRITE(irc
, message
);
354 snprintf(message
, PIPE_BUF
, "%s\r\n", &buf
[1]);
357 snprintf(message
, PIPE_BUF
, "%s\r\n", &buf
[1]);
359 if (message
[0] != '\0')
363 static void proc_server_cmd(char *buf
) {
364 char *argv
[TOK_LAST
], *cmd
= NULL
, *p
= NULL
;
367 if(!buf
|| *buf
=='\0')
370 for(i
= 0; i
< TOK_LAST
; i
++)
372 /* <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
373 <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
374 <command> ::= <letter> { <letter> } | <number> <number> <number>
375 <SPACE> ::= ' ' { ' ' }
376 <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
377 <middle> ::= <Any *non-empty* sequence of octets not including SPACE
378 or NUL or CR or LF, the first of which may not be ':'>
379 <trailing> ::= <Any, possibly *empty*, sequence of octets not including NUL or CR or LF>
382 if(buf
[0] == ':') { /* check prefix */
383 if (!(p
= strchr(buf
, ' '))) return;
385 for(++p
; *p
== ' '; p
++);
387 argv
[TOK_NICKSRV
] = &buf
[1];
388 if((p
= strchr(buf
, '!'))) {
390 argv
[TOK_USER
] = ++p
;
396 for(p
= cmd
; p
&& *p
!= 0; p
++)
397 if(*p
== '\r' || *p
== '\n')
400 if((p
= strchr(cmd
, ':'))) {
402 argv
[TOK_TEXT
] = ++p
;
405 tokenize(&argv
[TOK_CMD
], TOK_LAST
- TOK_CMD
, cmd
, ' ');
407 if(!argv
[TOK_CMD
] || !strncmp("PONG", argv
[TOK_CMD
], 5)) {
409 } else if(!strncmp("PING", argv
[TOK_CMD
], 5)) {
410 snprintf(message
, PIPE_BUF
, "PONG %s\r\n", argv
[TOK_TEXT
]);
413 } else if(!argv
[TOK_NICKSRV
] || !argv
[TOK_USER
]) { /* server command */
414 snprintf(message
, PIPE_BUF
, "%s%s", argv
[TOK_ARG
] ? argv
[TOK_ARG
] : "", argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
415 print_out(0, message
);
417 } else if(!strncmp("ERROR", argv
[TOK_CMD
], 6))
418 snprintf(message
, PIPE_BUF
, "-!- error %s", argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "unknown");
419 else if(!strncmp("JOIN", argv
[TOK_CMD
], 5)) {
420 if(argv
[TOK_TEXT
] != NULL
){
421 p
= strchr(argv
[TOK_TEXT
], ' ');
425 argv
[TOK_CHAN
] = argv
[TOK_TEXT
];
426 snprintf(message
, PIPE_BUF
, "-!- %s(%s) has joined %s", argv
[TOK_NICKSRV
], argv
[TOK_USER
], argv
[TOK_TEXT
]);
427 } else if(!strncmp("PART", argv
[TOK_CMD
], 5)) {
428 snprintf(message
, PIPE_BUF
, "-!- %s(%s) has left %s", argv
[TOK_NICKSRV
], argv
[TOK_USER
], argv
[TOK_CHAN
]);
429 } else if(!strncmp("MODE", argv
[TOK_CMD
], 5))
430 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] : "");
431 else if(!strncmp("QUIT", argv
[TOK_CMD
], 5))
432 snprintf(message
, PIPE_BUF
, "-!- %s(%s) has quit \"%s\"", argv
[TOK_NICKSRV
], argv
[TOK_USER
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
433 else if(!strncmp("NICK", argv
[TOK_CMD
], 5))
434 snprintf(message
, PIPE_BUF
, "-!- %s changed nick to %s", argv
[TOK_NICKSRV
], argv
[TOK_TEXT
]);
435 else if(!strncmp("TOPIC", argv
[TOK_CMD
], 6))
436 snprintf(message
, PIPE_BUF
, "-!- %s changed topic to \"%s\"", argv
[TOK_NICKSRV
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
437 else if(!strncmp("KICK", argv
[TOK_CMD
], 5))
438 snprintf(message
, PIPE_BUF
, "-!- %s kicked %s (\"%s\")", argv
[TOK_NICKSRV
], argv
[TOK_ARG
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
439 else if(!strncmp("NOTICE", argv
[TOK_CMD
], 7))
440 snprintf(message
, PIPE_BUF
, "-!- \"%s\")", argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
441 else if(!strncmp("PRIVMSG", argv
[TOK_CMD
], 8))
442 snprintf(message
, PIPE_BUF
, "<%s> %s", argv
[TOK_NICKSRV
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
443 if(!argv
[TOK_CHAN
] || !strncmp(argv
[TOK_CHAN
], nick
, strlen(nick
)))
444 print_out(argv
[TOK_NICKSRV
], message
);
446 print_out(argv
[TOK_CHAN
], message
);
450 static int read_line(int fd
, size_t res_len
, char *buf
, size_t from_srv
) {
452 static int read_line(int fd
, size_t res_len
, char *buf
) {
457 if(READ(fd
, &c
, sizeof(char)) != sizeof(char))
461 while(c
!= '\n' && i
< res_len
);
462 buf
[i
- 1] = 0; /* eliminates '\n' */
466 static void handle_channels_input(Channel
*c
) {
467 static char buf
[PIPE_BUF
];
469 if(read_line(c
->fd
, PIPE_BUF
, buf
, 0) == -1) {
471 if(read_line(c
->fd
, PIPE_BUF
, buf
) == -1) {
474 int fd
= open_channel(c
->name
);
481 proc_channels_input(c
, buf
);
484 static void handle_server_output() {
485 static char buf
[PIPE_BUF
];
487 if(read_line(irc
->irc
, PIPE_BUF
, buf
, 1) == -1) {
489 if(read_line(irc
, PIPE_BUF
, buf
) == -1) {
491 perror("ii: remote host closed connection");
494 proc_server_cmd(buf
);
502 char ping_msg
[17] = "PING localhost\r\n";
508 FD_SET(irc
->irc
, &rd
);
513 for(c
= channels
; c
; c
= c
->next
) {
521 r
= select(maxfd
+ 1, &rd
, 0, 0, &tv
);
525 perror("ii: error on select()");
528 if(time(NULL
) - last_response
>= PING_TIMEOUT
) {
529 print_out(NULL
, "-!- ii shutting down: ping timeout");
532 WRITE(irc
, ping_msg
);
536 if(FD_ISSET(irc
->irc
, &rd
)) {
538 if(FD_ISSET(irc
, &rd
)) {
540 handle_server_output();
541 last_response
= time(NULL
);
543 for(c
= channels
; c
; c
= c
->next
)
544 if(FD_ISSET(c
->fd
, &rd
))
545 handle_channels_input(c
);
549 int main(int argc
, char *argv
[]) {
551 unsigned short port
= SERVER_PORT
;
552 char *key
= NULL
, *fullname
= NULL
, *dir
= NULL
;
554 char prefix
[_POSIX_PATH_MAX
] = "irc", *pmsg
= message
+ 17;
556 char prefix
[_POSIX_PATH_MAX
] = "irc";
559 for(i
= 1; (i
+ 1 < argc
) && (argv
[i
][0] == '-'); i
++) {
560 switch (argv
[i
][1]) {
562 case 'e': use_ssl
= 1; break;
564 case 'i': snprintf(prefix
,sizeof(prefix
),"%s", argv
[++i
]); break;
565 case 's': host
= argv
[++i
]; break;
566 case 'p': port
= strtol(argv
[++i
], NULL
, 10); break;
567 case 'n': snprintf(nick
,sizeof(nick
),"%s", argv
[++i
]); break;
568 case 'k': key
= argv
[++i
]; break;
569 case 'f': fullname
= argv
[++i
]; break;
570 case 'd': dir
= argv
[++i
]; break;
571 default: usage(); break;
574 if(i
!= argc
) usage();
576 if(use_ssl
) port
= port
== SERVER_PORT
? SSL_SERVER_PORT
: port
;
579 if(!snprintf(path
, sizeof(path
), "%s/%s", prefix
, dir
? dir
: host
)) {
580 fprintf(stderr
, "%s", "ii: path to irc directory too long\n");
583 create_dirtree(path
);
585 add_channel(""); /* master channel */
586 login(key
, fullname
);
589 snprintf(message
, PIPE_BUF
, "MD5 Fingerprint: ");
590 for(i
= 0; strlen(message
) < PIPE_BUF
&& i
< fp_len
; i
++) {
591 snprintf(pmsg
, PIPE_BUF
, i
> 0 ? ":%02X" : "%02X", fp
[i
]);
595 print_out(NULL
, message
);