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