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 SSL_SERVER_PORT 6697
30 #define IS_CHANNEL(s) (((s)[0]=='#')||((s)[0]=='&')||((s)[0]=='+')||((s)[0]=='!'))
32 #define SSL_SERVER_PORT 6697
33 #define WRITE(conn, msg) (use_ssl ? SSL_write(irc->sslHandle, msg, strlen(msg)) : write(conn->irc, msg, strlen(msg)))
34 #define READ(fd, buf, s) (from_srv && use_ssl ? SSL_read(irc->sslHandle, buf, s) : read(fd, buf, s))
36 #define WRITE(irc, msg) (write(irc, msg, strlen(msg)))
37 #define READ(fd, buf, s) (read(fd, buf, s))
39 enum { TOK_NICKSRV
= 0, TOK_USER
, TOK_CMD
, TOK_CHAN
, TOK_ARG
, TOK_TEXT
, TOK_LAST
};
41 typedef struct Channel Channel
;
56 static size_t use_ssl
= 0;
58 static unsigned char fp
[EVP_MAX_MD_SIZE
];
63 static time_t last_response
;
64 static Channel
*channels
= NULL
;
65 static char *host
= "irc.freenode.net";
66 static char nick
[32] = "anonymous"; /* might change while running */
67 static char path
[_POSIX_PATH_MAX
];
68 static char message
[PIPE_BUF
]; /* message buf used for communication */
72 "ii - irc it - " VERSION
"\n"
73 "(C)opyright MMV-MMVI Anselm R. Garbe\n"
74 "(C)opyright MMV-MMXI Nico Golde\n"
75 "usage: ii [-i <irc dir>] [-s <host>] [-p <port>]\n"
76 " [-n <nick>] [-k <password>] [-f <fullname>]\n"
78 " [-e] [-d <directory>]\n");
80 " [-d <directory>]\n");
85 static char *striplower(char *s
) {
87 for(p
= s
; p
&& *p
; p
++) {
88 if(*p
== '/') *p
= '_';
94 /* creates directories top-down, if necessary */
95 static void create_dirtree(const char *dir
) {
100 snprintf(tmp
, sizeof(tmp
),"%s",dir
);
102 if(tmp
[len
- 1] == '/')
104 for(p
= tmp
+ 1; *p
; p
++)
113 static int get_filepath(char *filepath
, size_t len
, char *channel
, char *file
) {
115 if(!snprintf(filepath
, len
, "%s/%s", path
, channel
))
117 create_dirtree(filepath
);
118 return snprintf(filepath
, len
, "%s/%s/%s", path
, channel
, file
);
120 return snprintf(filepath
, len
, "%s/%s", path
, file
);
123 static void create_filepath(char *filepath
, size_t len
, char *channel
, char *suffix
) {
124 if(!get_filepath(filepath
, len
, striplower(channel
), suffix
)) {
125 fprintf(stderr
, "%s", "ii: path to irc directory too long\n");
130 static int open_channel(char *name
) {
131 static char infile
[256];
132 create_filepath(infile
, sizeof(infile
), name
, "in");
133 if(access(infile
, F_OK
) == -1)
134 mkfifo(infile
, S_IRWXU
);
135 return open(infile
, O_RDONLY
| O_NONBLOCK
, 0);
138 static void add_channel(char *cname
) {
141 char *name
= striplower(cname
);
143 for(c
= channels
; c
; c
= c
->next
)
144 if(!strcmp(name
, c
->name
))
145 return; /* already handled */
147 fd
= open_channel(name
);
149 printf("ii: exiting, cannot create in channel: %s\n", name
);
152 c
= calloc(1, sizeof(Channel
));
154 perror("ii: cannot allocate memory");
157 if(!channels
) channels
= c
;
163 c
->name
= strdup(name
);
166 static void rm_channel(Channel
*c
) {
168 if(channels
== c
) channels
= channels
->next
;
170 for(p
= channels
; p
&& p
->next
!= c
; p
= p
->next
);
178 static void login(char *key
, char *fullname
) {
179 if(key
) snprintf(message
, PIPE_BUF
,
180 "PASS %s\r\nNICK %s\r\nUSER %s localhost * :%s\r\n", key
,
181 nick
, nick
, fullname
? fullname
: nick
);
182 else snprintf(message
, PIPE_BUF
, "NICK %s\r\nUSER %s localhost * :%s\r\n",
183 nick
, nick
, fullname
? fullname
: nick
);
184 WRITE(irc
, message
); /* login */
188 static conn
*ssl_connect(int fd
) {
191 c
= malloc(sizeof(conn
));
193 perror("ii: cannot allocate memory");
200 c
->sslContext
= NULL
;
201 SSL_load_error_strings();
203 c
->sslContext
= SSL_CTX_new(SSLv23_client_method());
204 if(c
->sslContext
== NULL
)
205 ERR_print_errors_fp(stderr
);
206 c
->sslHandle
= SSL_new(c
->sslContext
);
207 if(!SSL_set_fd(c
->sslHandle
, c
->irc
) || SSL_connect(c
->sslHandle
) != 1)
208 ERR_print_errors_fp(stderr
);
209 if(!X509_digest(SSL_get_peer_certificate(c
->sslHandle
), EVP_md5(), fp
, &fp_len
))
210 ERR_print_errors_fp(stderr
);
216 static conn
*tcpopen(unsigned short port
) {
218 static int tcpopen(unsigned short port
) {
221 struct sockaddr_in sin
;
222 struct hostent
*hp
= gethostbyname(host
);
224 memset(&sin
, 0, sizeof(struct sockaddr_in
));
226 perror("ii: cannot retrieve host information");
229 sin
.sin_family
= AF_INET
;
230 memcpy(&sin
.sin_addr
, hp
->h_addr
, hp
->h_length
);
231 sin
.sin_port
= htons(port
);
232 if((fd
= socket(AF_INET
, SOCK_STREAM
, 0)) < 0) {
233 perror("ii: cannot create socket");
236 if(connect(fd
, (const struct sockaddr
*) &sin
, sizeof(sin
)) < 0) {
237 perror("ii: cannot connect to host");
242 return ssl_connect(fd
);
248 static size_t tokenize(char **result
, size_t reslen
, char *str
, char delim
) {
249 char *p
= NULL
, *n
= NULL
;
254 for(n
= str
; *n
== ' '; n
++);
256 for(i
= 0; *n
!= 0;) {
259 if(i
> TOK_CHAN
- TOK_CMD
&& strtol(result
[0], NULL
, 10) > 0) delim
=':'; /* workaround non-RFC compliant messages */
267 if(i
<reslen
&& p
< n
&& strlen(p
))
269 return i
; /* number of tokens */
272 static void print_out(char *channel
, char *buf
) {
273 static char outfile
[256], server
[256], buft
[18];
277 if(channel
) snprintf(server
, sizeof(server
), "-!- %s", channel
);
278 if(strstr(buf
, server
)) channel
="";
279 create_filepath(outfile
, sizeof(outfile
), channel
, "out");
280 if(!(out
= fopen(outfile
, "a"))) return;
281 if(channel
&& channel
[0]) add_channel(channel
);
283 strftime(buft
, sizeof(buft
), "%F %R", localtime(&t
));
284 fprintf(out
, "%s %s\n", buft
, buf
);
288 static void proc_channels_privmsg(char *channel
, char *buf
) {
289 snprintf(message
, PIPE_BUF
, "<%s> %s", nick
, buf
);
290 print_out(channel
, message
);
291 snprintf(message
, PIPE_BUF
, "PRIVMSG %s :%s\r\n", channel
, buf
);
295 static void proc_channels_input(Channel
*c
, char *buf
) {
298 if(buf
[0] && buf
[0] != '/') {
299 proc_channels_privmsg(c
->name
, buf
);
303 if(buf
[2] == ' ' || buf
[2] == '\0') switch (buf
[1]) {
305 if(buf
[3] == ' ' || buf
[3] == '\0') return;
306 p
= strchr(&buf
[3], ' ');
308 if(IS_CHANNEL(&buf
[3])){
309 if(p
&& strlen(p
+ 1)) snprintf(message
, PIPE_BUF
, "JOIN %s %s\r\n", &buf
[3], p
+ 1); /* password protected channel */
310 else snprintf(message
, PIPE_BUF
, "JOIN %s\r\n", &buf
[3]);
311 add_channel(&buf
[3]);
313 else if(p
&& strlen(p
+ 1)) {
314 add_channel(&buf
[3]);
315 proc_channels_privmsg(&buf
[3], p
+ 1);
320 if(IS_CHANNEL(&buf
[3])){
321 p
= strchr(&buf
[3], ' ');
323 if(p
&& strlen(p
+ 1)) snprintf(message
, PIPE_BUF
, "TOPIC %s :%s\r\n", &buf
[3], p
+ 1);
324 else snprintf(message
, PIPE_BUF
, "TOPIC %s\r\n", &buf
[3]);
327 if(c
->name
[0] == 0) return;
328 if(strlen(buf
)>3) snprintf(message
, PIPE_BUF
, "TOPIC %s :%s\r\n", c
->name
, &buf
[3]);
329 else snprintf(message
, PIPE_BUF
, "TOPIC %s\r\n", c
->name
);
334 snprintf(message
, PIPE_BUF
, "-!- %s is away \"%s\"", nick
, &buf
[3]);
335 print_out(c
->name
, message
);
336 snprintf(message
, PIPE_BUF
, "AWAY :%s\r\n", &buf
[3]);
337 } else snprintf(message
, PIPE_BUF
, "AWAY\r\n");
341 snprintf(nick
, sizeof(nick
),"%s", &buf
[3]);
342 snprintf(message
, PIPE_BUF
, "NICK %s\r\n", &buf
[3]);
346 if(c
->name
[0] == 0) return;
347 if(strlen(buf
)>3) snprintf(message
, PIPE_BUF
, "PART %s :%s\r\n", c
->name
, &buf
[3]);
348 else snprintf(message
, PIPE_BUF
, "PART %s\r\n", c
->name
);
349 if(IS_CHANNEL(c
->name
)) WRITE(irc
, message
);
355 snprintf(message
, PIPE_BUF
, "%s\r\n", &buf
[1]);
358 snprintf(message
, PIPE_BUF
, "%s\r\n", &buf
[1]);
360 if (message
[0] != '\0')
364 static void proc_server_cmd(char *buf
) {
365 char *argv
[TOK_LAST
], *cmd
= NULL
, *p
= NULL
;
368 if(!buf
|| *buf
=='\0')
371 for(i
= 0; i
< TOK_LAST
; i
++)
373 /* <message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
374 <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
375 <command> ::= <letter> { <letter> } | <number> <number> <number>
376 <SPACE> ::= ' ' { ' ' }
377 <params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
378 <middle> ::= <Any *non-empty* sequence of octets not including SPACE
379 or NUL or CR or LF, the first of which may not be ':'>
380 <trailing> ::= <Any, possibly *empty*, sequence of octets not including NUL or CR or LF>
383 if(buf
[0] == ':') { /* check prefix */
384 if (!(p
= strchr(buf
, ' '))) return;
386 for(++p
; *p
== ' '; p
++);
388 argv
[TOK_NICKSRV
] = &buf
[1];
389 if((p
= strchr(buf
, '!'))) {
391 argv
[TOK_USER
] = ++p
;
397 for(p
= cmd
; p
&& *p
!= 0; p
++)
398 if(*p
== '\r' || *p
== '\n')
401 if((p
= strchr(cmd
, ':'))) {
403 argv
[TOK_TEXT
] = ++p
;
406 tokenize(&argv
[TOK_CMD
], TOK_LAST
- TOK_CMD
, cmd
, ' ');
408 if(!argv
[TOK_CMD
] || !strncmp("PONG", argv
[TOK_CMD
], 5)) {
410 } else if(!strncmp("PING", argv
[TOK_CMD
], 5)) {
411 snprintf(message
, PIPE_BUF
, "PONG %s\r\n", argv
[TOK_TEXT
]);
414 } else if(!argv
[TOK_NICKSRV
] || !argv
[TOK_USER
]) { /* server command */
415 snprintf(message
, PIPE_BUF
, "%s%s", argv
[TOK_ARG
] ? argv
[TOK_ARG
] : "", argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
416 print_out(0, message
);
418 } else if(!strncmp("ERROR", argv
[TOK_CMD
], 6))
419 snprintf(message
, PIPE_BUF
, "-!- error %s", argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "unknown");
420 else if(!strncmp("JOIN", argv
[TOK_CMD
], 5)) {
421 if(argv
[TOK_TEXT
] != NULL
){
422 p
= strchr(argv
[TOK_TEXT
], ' ');
426 argv
[TOK_CHAN
] = argv
[TOK_TEXT
];
427 snprintf(message
, PIPE_BUF
, "-!- %s(%s) has joined %s", argv
[TOK_NICKSRV
], argv
[TOK_USER
], argv
[TOK_TEXT
]);
428 } else if(!strncmp("PART", argv
[TOK_CMD
], 5)) {
429 snprintf(message
, PIPE_BUF
, "-!- %s(%s) has left %s", argv
[TOK_NICKSRV
], argv
[TOK_USER
], argv
[TOK_CHAN
]);
430 } else if(!strncmp("MODE", argv
[TOK_CMD
], 5))
431 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] : "");
432 else if(!strncmp("QUIT", argv
[TOK_CMD
], 5))
433 snprintf(message
, PIPE_BUF
, "-!- %s(%s) has quit \"%s\"", argv
[TOK_NICKSRV
], argv
[TOK_USER
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
434 else if(!strncmp("NICK", argv
[TOK_CMD
], 5))
435 snprintf(message
, PIPE_BUF
, "-!- %s changed nick to %s", argv
[TOK_NICKSRV
], argv
[TOK_TEXT
]);
436 else if(!strncmp("TOPIC", argv
[TOK_CMD
], 6))
437 snprintf(message
, PIPE_BUF
, "-!- %s changed topic to \"%s\"", argv
[TOK_NICKSRV
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
438 else if(!strncmp("KICK", argv
[TOK_CMD
], 5))
439 snprintf(message
, PIPE_BUF
, "-!- %s kicked %s (\"%s\")", argv
[TOK_NICKSRV
], argv
[TOK_ARG
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
440 else if(!strncmp("NOTICE", argv
[TOK_CMD
], 7))
441 snprintf(message
, PIPE_BUF
, "-!- \"%s\")", argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
442 else if(!strncmp("PRIVMSG", argv
[TOK_CMD
], 8))
443 snprintf(message
, PIPE_BUF
, "<%s> %s", argv
[TOK_NICKSRV
], argv
[TOK_TEXT
] ? argv
[TOK_TEXT
] : "");
444 if(!argv
[TOK_CHAN
] || !strncmp(argv
[TOK_CHAN
], nick
, strlen(nick
)))
445 print_out(argv
[TOK_NICKSRV
], message
);
447 print_out(argv
[TOK_CHAN
], message
);
451 static int read_line(int fd
, size_t res_len
, char *buf
, size_t from_srv
) {
453 static int read_line(int fd
, size_t res_len
, char *buf
) {
458 if(READ(fd
, &c
, sizeof(char)) != sizeof(char))
462 while(c
!= '\n' && i
< res_len
);
463 buf
[i
- 1] = 0; /* eliminates '\n' */
467 static void handle_channels_input(Channel
*c
) {
468 static char buf
[PIPE_BUF
];
470 if(read_line(c
->fd
, PIPE_BUF
, buf
, 0) == -1) {
472 if(read_line(c
->fd
, PIPE_BUF
, buf
) == -1) {
475 int fd
= open_channel(c
->name
);
482 proc_channels_input(c
, buf
);
485 static void handle_server_output() {
486 static char buf
[PIPE_BUF
];
488 if(read_line(irc
->irc
, PIPE_BUF
, buf
, 1) == -1) {
490 if(read_line(irc
, PIPE_BUF
, buf
) == -1) {
492 perror("ii: remote host closed connection");
495 proc_server_cmd(buf
);
503 char ping_msg
[17] = "PING localhost\r\n";
509 FD_SET(irc
->irc
, &rd
);
514 for(c
= channels
; c
; c
= c
->next
) {
522 r
= select(maxfd
+ 1, &rd
, 0, 0, &tv
);
526 perror("ii: error on select()");
529 if(time(NULL
) - last_response
>= PING_TIMEOUT
) {
530 print_out(NULL
, "-!- ii shutting down: ping timeout");
533 WRITE(irc
, ping_msg
);
537 if(FD_ISSET(irc
->irc
, &rd
)) {
539 if(FD_ISSET(irc
, &rd
)) {
541 handle_server_output();
542 last_response
= time(NULL
);
544 for(c
= channels
; c
; c
= c
->next
)
545 if(FD_ISSET(c
->fd
, &rd
))
546 handle_channels_input(c
);
550 int main(int argc
, char *argv
[]) {
552 unsigned short port
= SERVER_PORT
;
553 char *key
= NULL
, *fullname
= NULL
, *dir
= NULL
;
555 char prefix
[_POSIX_PATH_MAX
] = "irc", *pmsg
= message
+ 17;
557 char prefix
[_POSIX_PATH_MAX
] = "irc";
560 for(i
= 1; (i
+ 1 < argc
) && (argv
[i
][0] == '-'); i
++) {
561 switch (argv
[i
][1]) {
563 case 'e': use_ssl
= 1; break;
565 case 'i': snprintf(prefix
,sizeof(prefix
),"%s", argv
[++i
]); break;
566 case 's': host
= argv
[++i
]; break;
567 case 'p': port
= strtol(argv
[++i
], NULL
, 10); break;
568 case 'n': snprintf(nick
,sizeof(nick
),"%s", argv
[++i
]); break;
569 case 'k': key
= argv
[++i
]; break;
570 case 'f': fullname
= argv
[++i
]; break;
571 case 'd': dir
= argv
[++i
]; break;
572 default: usage(); break;
575 if(i
!= argc
) usage();
577 if(use_ssl
) port
= port
== SERVER_PORT
? SSL_SERVER_PORT
: port
;
580 if(!snprintf(path
, sizeof(path
), "%s/%s", prefix
, dir
? dir
: host
)) {
581 fprintf(stderr
, "%s", "ii: path to irc directory too long\n");
584 create_dirtree(path
);
586 add_channel(""); /* master channel */
587 login(key
, fullname
);
590 snprintf(message
, PIPE_BUF
, "MD5 Fingerprint: ");
591 for(i
= 0; strlen(message
) < PIPE_BUF
&& i
< fp_len
; i
++) {
592 snprintf(pmsg
, PIPE_BUF
, i
> 0 ? ":%02X" : "%02X", fp
[i
]);
596 print_out(NULL
, message
);