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