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