]> git.gir.st - ircpipe.git/blob - ircpipe.c
add examples, split out caveats, and other readme improvements
[ircpipe.git] / ircpipe.c
1 #define _POSIX_C_SOURCE 200112L /* getopt(>=2), strtok_r(*), getaddrinfo(>=200112L), clock_gettime(>=199309L), sigaction(*) */
2 #include <poll.h>
3 #include <time.h>
4 #include <stdio.h>
5 #include <signal.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <unistd.h>
9 #include <sys/time.h>
10 #include <sys/types.h>
11 #include <sys/socket.h>
12 #include <netdb.h>
13
14 #include <tls.h>
15
16 #define PROGNAME "ircpipe"
17 #define VERSION "0.1"
18
19 #define DEFAULT_TLS NO_TLS
20 #define DEFAULT_PORT_TCP "6667"
21 #define DEFAULT_PORT_TLS "6697"
22
23 #define POLL_TIMEOUT 100 /*ms*/
24 #define PING_INTERVAL 120000 /*ms*/
25 #define PONG_TIMEOUT 2000 /*ms*/
26 #define SETUP_TIMEOUT 15000 /*ms*/
27
28 #define STR_(x) #x
29 #define STR(x) STR_(x)
30 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
31 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", gai_strerror(err));exit(1);}
32 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", tls_error(ctx)), 1)), 0)
33
34 enum pass_type_e {
35 NO_PASSWD,
36 SERVER_PASSWD,
37 SASL_PLAIN_PASSWD
38 };
39
40 enum tls_use_e {
41 NO_TLS,
42 USE_TLS,
43 INSECURE_TLS
44 };
45
46 typedef struct {
47 int fd; /* always contains the underlying file descriptor */
48 struct tls *tls; /* tls context, or NULL with plain socket */
49 } sock_t;
50 #define _IMPLFN(fn, sock, buf, sz) ( \
51 sock.tls \
52 ? tls_ ## fn(sock.tls, buf, sz) \
53 : fn(sock.fd, buf, sz) \
54 )
55 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
56 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
57
58 void irc_help(const char *exe, const int code) {
59 fprintf(stderr, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT]\n", exe);
60 exit(code);
61 }
62
63 sock_t irc_connect(const char *host, const char *port, const int tls, const char *ca_file) {
64 sock_t sock;
65 struct addrinfo *results, *r;
66 struct timeval timeout;
67
68 int err = getaddrinfo(host, port, NULL, &results); OR_DIE_gai(err); /*unable to resolve*/
69
70 timeout.tv_sec = 10;
71 timeout.tv_usec = 0;
72
73 for (r = results; r != NULL; r = r->ai_next) {
74 sock.fd = socket(r->ai_family, SOCK_STREAM, 0);
75 if (sock.fd < 0) continue;
76
77 setsockopt(sock.fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout) OR_DIE;
78 if (connect(sock.fd, r->ai_addr, r->ai_addrlen) == 0)
79 break; /* successfully connected */
80
81 close(sock.fd); /* failed, try next addr */
82 }
83
84 if (r == NULL) {
85 /* all failed; abort. */
86 sock.fd = -1;
87 } else {
88 /* connection established. */
89 if (tls != NO_TLS) {
90 struct tls *ctx = tls_client();
91 struct tls_config *cfg = tls_config_new();
92
93 if (tls == INSECURE_TLS) {
94 tls_config_insecure_noverifycert(cfg);
95 tls_config_insecure_noverifyname(cfg);
96 tls_config_insecure_noverifytime(cfg);
97 tls_config_set_ciphers(cfg, "legacy"); /* even more: 'insecure' */
98 }
99 tls_config_set_dheparams(cfg, "auto") OR_DIE_tls(ctx);
100 if (ca_file) tls_config_set_ca_file(cfg, ca_file) OR_DIE_tls(ctx);
101 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
102
103 tls_configure(ctx, cfg) OR_DIE_tls(ctx);
104 tls_config_free(cfg);
105 tls_connect_socket(ctx, sock.fd, host) OR_DIE_tls(ctx);
106 tls_handshake(ctx) OR_DIE_tls(ctx);
107
108 sock.tls = ctx;
109 } else sock.tls = NULL;
110 /* connect timeout here */
111 }
112
113 freeaddrinfo(results);
114 return sock;
115 }
116
117 enum { /* requested command: */
118 NO_CMD = 0,
119 NICK = 1<<0,
120 JOIN = 1<<1,
121 PING = 1<<2,
122 ERRS = 1<<3
123 };
124 int irc_answer(const sock_t sock, char *buf, const unsigned int command) {
125 unsigned int seen = 0;
126 char *saveptr;
127 char *line = strtok_r(buf, "\r\n", &saveptr);
128 /*TODO: it often happens that we take multiple calls to read() all the available lines (e.g. large motd, NAMES message). when this happens, one call to read() will return an incomplete line, and the next will start in the middle of a line. this can't be parsed properly! we need to check if the last line ends with a newline. that's hard because we use strtok which removes newlines. on the second read we should either skip over the first partial line or better, defer parsing the last line of the first read until we have the complete line.*/
129 do {
130 /* skip over prefix (servername): */
131 if (line[0] == ':')
132 while (*line && *line++ != ' ');
133
134 /* look for command responses or relevant error numerics: */
135 switch (command) {
136 case PING: seen |= PING * (strncmp(line, "PONG ", 5)==0); break;
137 case JOIN: seen |= JOIN * (strncmp(line, "JOIN ", 5)==0);
138 seen |= ERRS * (strncmp(line, "403 ", 4)==0);
139 seen |= ERRS * (strncmp(line, "405 ", 4)==0);
140 seen |= ERRS * (strncmp(line, "471 ", 4)==0);
141 seen |= ERRS * (strncmp(line, "473 ", 4)==0);
142 seen |= ERRS * (strncmp(line, "474 ", 4)==0);
143 seen |= ERRS * (strncmp(line, "475 ", 4)==0);
144 seen |= ERRS * (strncmp(line, "476 ", 4)==0);
145 seen |= ERRS * (strncmp(line, "477 ", 4)==0); break;
146 case NICK: seen |= NICK * (strncmp(line, "001 ", 4)==0);
147 seen |= ERRS * (strncmp(line, "432 ", 4)==0);
148 seen |= ERRS * (strncmp(line, "433 ", 4)==0);
149 seen |= ERRS * (strncmp(line, "436 ", 4)==0);
150 seen |= ERRS * (strncmp(line, "464 ", 4)==0);
151 seen |= ERRS * (strncmp(line, "902 ", 4)==0);
152 seen |= ERRS * (strncmp(line, "904 ", 4)==0); break;
153 }
154 /* look for common error numerics if any command was given */
155 if (command & (NICK|JOIN)) {
156 seen |= ERRS * (strncmp(line, "400 ", 4)==0);
157 seen |= ERRS * (strncmp(line, "421 ", 4)==0);
158 seen |= ERRS * (strncmp(line, "465 ", 4)==0);
159 }
160 /* always look for a fatal error */
161 if (strncmp(line, "ERROR ", 6)==0) seen |= ERRS;
162
163 if (seen & ERRS) {
164 fprintf(stderr, "IRC error: %s\n", line);
165 exit(1);
166 }
167
168 /* reply to pings: */
169 if (strncmp(line, "PING ", 5) == 0) {
170 int n = strlen(line);
171 int crlf = line[n+1] == '\n'; /* strtok only removes first delimeter */
172 line[1] = 'O'; /* PING :foo -> PONG :foo */
173 line[n] = crlf ? '\r' : '\n'; /* re-terminate after strtok */
174 WRITE(sock, line, n+crlf+1);
175 }
176 } while ((line = strtok_r(NULL, "\r\n", &saveptr)));
177
178 return seen;
179 }
180
181 int irc_base64(char *buf, int n) {
182 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
183 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
184 buf[n+1] = buf[n+2] = buf[l] = '\0';
185 for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) {
186 v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0;
187 buf[o+0] = b[v>>18 & 0x3f];
188 buf[o+1] = b[v>>12 & 0x3f];
189 buf[o+2] = (i+1<n)? b[v>>06 & 0x3f]:'=';
190 buf[o+3] = (i+2<n)? b[v>>00 & 0x3f]:'=';
191 }
192 return l;
193 }
194
195 long irc_time(void) {
196 struct timespec t;
197 clock_gettime(CLOCK_MONOTONIC, &t) OR_DIE;
198 return t.tv_sec*1000 + t.tv_nsec / 1000000; /* milliseconds */
199 }
200
201 int irc_wait(const sock_t sock, const int outfd, int cmd, char* buf) {
202 int n;
203 long start = irc_time();
204 struct pollfd fds[1];
205 fds[0].fd = sock.fd;
206 fds[0].events = POLLIN;
207
208 for (;;) { /* note: reusing callee's buf */
209 if (poll(fds, 1, POLL_TIMEOUT)) {
210 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
211 if (n == 0) return -1; /* server closed connection */
212 write(outfd, buf, n);
213 n = irc_answer(sock, buf, cmd);
214 if (n & cmd) return 0;
215 if (irc_time() - start > SETUP_TIMEOUT) {
216 fprintf(stderr, "IRC setup timeout\n");
217 exit(1);
218 }
219 }
220 }
221 }
222
223 int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) {
224 char buf[BUFSIZ];
225 int n;
226
227 if (pass_type == SASL_PLAIN_PASSWD) {
228 n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n");
229 WRITE(sock, buf, n);
230 } else if (pass_type == SERVER_PASSWD) {
231 n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass);
232 WRITE(sock, buf, n);
233 }
234
235 n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick);
236 WRITE(sock, buf, n);
237 n = snprintf(buf, BUFSIZ, "USER %s 0 * :%s\r\n", nick, nick);
238 WRITE(sock, buf, n);
239
240 if (pass_type == SASL_PLAIN_PASSWD) {
241 /* note: should assert strlen(pass) < 300 for spec compliance */
242 /* should wait for 'CAP <nick|*> ACK :<...>' */
243 WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20);
244 /* server sends 'AUTHENTICATE +' */
245 n = snprintf(buf, BUFSIZ, "AUTHENTICATE %s%c%s%c%s", nick, 0, nick, 0, pass);
246 n = irc_base64(buf+13, n-13)+13; /*13==strlen("AUTHENTICATE ")*/
247 n += snprintf(buf+n, BUFSIZ-n, "\r\n");
248 WRITE(sock, buf, n);
249 /* wait for response 900+903 (ok) or 902/904 (err) */
250 WRITE(sock, "CAP END\r\n", 9);
251 }
252
253 /* block until we get a RPL_WELCOME or an error: */
254 n = irc_wait(sock, outfd, NICK, buf);
255 if (n < 0) return n;
256
257 if (chan) {
258 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
259 WRITE(sock, buf, n);
260
261 /* block until we get a JOIN response or an error: */
262 n = irc_wait(sock, outfd, JOIN, buf);
263 if (n < 0) return n;
264 }
265
266 return 0;
267 }
268
269 int irc_poll(const sock_t sock, const int infd, const int outfd) {
270 int n;
271 char buf[BUFSIZ];
272
273 int want_pong = 0;
274 long recv_ts = irc_time();
275
276 enum { IRC, CLI };
277 struct pollfd fds[2];
278 fds[IRC].fd = sock.fd;
279 fds[IRC].events = POLLIN;
280 fds[CLI].fd = infd;
281 fds[CLI].events = POLLIN;
282
283 for (;;) {
284 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
285
286 /* todo: should retry on EINTR, EAGAIN */
287 if (fds[IRC].revents & POLLIN) {
288 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
289 if (n == 0) return -1; /* server closed connection */
290 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
291 write(outfd, buf, n);
292 if (irc_answer(sock, buf, want_pong?PING:NO_CMD) & PING)
293 want_pong = 0;
294 recv_ts = irc_time();
295 }
296 if (fds[CLI].revents & POLLIN) {
297 n = read(infd, buf, BUFSIZ); buf[n] = '\0';
298 if (n == 0) return 0; /* we closed connection */
299 n = WRITE(sock, buf, n);
300 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
301 }
302 if (fds[IRC].revents & POLLOUT) { /* needed for TLS only */
303 n = WRITE(sock, buf, n);
304 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
305 }
306
307 if (want_pong && irc_time() - recv_ts > PING_INTERVAL + PONG_TIMEOUT) {
308 /* pong timeout reached, abort. */
309 fprintf(stderr, "PONG timeout\n");
310 return -1;
311 } else if (!want_pong && irc_time() - recv_ts > PING_INTERVAL) {
312 /* haven't rx'd anything in a while, sending ping. */
313 WRITE(sock, "PING :ircpipe\r\n", 15);
314 want_pong = 1;
315 }
316 }
317 }
318
319 void irc_cleanup(const sock_t sock) {
320 WRITE(sock, "QUIT :ircpipe\r\n", 15);
321 if (sock.tls) {
322 tls_close(sock.tls);
323 tls_free(sock.tls);
324 }
325 shutdown(sock.fd, SHUT_RDWR);
326 close(sock.fd);
327 }
328
329 sock_t sock;
330
331 void irc_sighandler(int signum) {
332 switch (signum) {
333 case SIGHUP:
334 case SIGINT:
335 case SIGTERM:
336 irc_cleanup(sock);
337 exit(signum);
338 }
339 }
340
341 int main(int argc, char **argv) {
342 char *host = NULL;
343 char *port = NULL;
344 char *nick = NULL;
345 char *pass = NULL;
346 char *chan = NULL;
347 int tls = DEFAULT_TLS;
348 char *ca_file = NULL;
349 int pass_type = NO_PASSWD;
350
351 int rv;
352 struct sigaction act;
353
354 int opt; opterr = 0;
355
356 pass = getenv("IRC_PASSWD");
357 ca_file = getenv("IRC_CAFILE");
358
359 while ((opt = getopt(argc, argv, "n:j:pPsSkhV")) != -1) {
360 switch (opt) {
361 case 'n': nick = optarg; break;
362 case 'p': pass_type = SERVER_PASSWD; break;
363 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
364 case 's': tls = USE_TLS; break;
365 case 'S': tls = NO_TLS; break;
366 case 'k': tls = INSECURE_TLS; break;
367 case 'j': chan = optarg; break;
368 case 'V': printf(PROGNAME " " VERSION "\n"); return 0;
369 default: irc_help(argv[0], opt != 'h');
370 }
371 }
372
373 if (optind < argc) {
374 host = argv[optind++];
375 } else {
376 /* too few positional arguments */
377 fprintf(stderr, "missing HOST\n");
378 irc_help(argv[0], 1);
379 }
380 if (optind < argc) {
381 port = argv[optind++];
382 } else {
383 port = (tls == NO_TLS)
384 ? DEFAULT_PORT_TCP
385 : DEFAULT_PORT_TLS;
386 }
387 if (optind < argc) {
388 /* too many positional arguments */
389 fprintf(stderr, "too many args\n");
390 irc_help(argv[0], 1);
391 }
392
393 if (pass_type != NO_PASSWD && pass == NULL) {
394 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
395 exit(1);
396 }
397
398 memset(&act, 0, sizeof act);
399 act.sa_handler = irc_sighandler;
400 sigaction(SIGHUP, &act, NULL);
401 sigaction(SIGINT, &act, NULL);
402 sigaction(SIGTERM, &act, NULL);
403
404 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
405 irc_setup(sock, 1, nick, pass, pass_type, chan);
406 rv = irc_poll(sock, 0, 1);
407 irc_cleanup(sock);
408
409 return (rv < 0);
410 }
Imprint / Impressum