]> git.gir.st - ircpipe.git/blob - ircpipe.c
fix segfault on D-line
[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
24 #define STR_(x) #x
25 #define STR(x) STR_(x)
26 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
27 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", gai_strerror(err));exit(1);}
28 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", tls_error(ctx)), 1)), 0)
29
30 enum pass_type_e {
31 NO_PASSWD,
32 SERVER_PASSWD,
33 SASL_PLAIN_PASSWD
34 };
35
36 enum tls_use_e {
37 NO_TLS,
38 USE_TLS,
39 INSECURE_TLS
40 };
41
42 typedef struct {
43 int fd; /* always contains the underlying file descriptor */
44 struct tls *tls; /* tls context, or NULL with plain socket */
45 } sock_t;
46 #define _IMPLFN(fn, sock, buf, sz) ( \
47 sock.tls \
48 ? tls_ ## fn(sock.tls, buf, sz) \
49 : fn(sock.fd, buf, sz) \
50 )
51 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
52 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
53
54 void irc_help(const char *exe, const int code) {
55 fprintf(stderr, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT]\n", exe);
56 exit(code);
57 }
58
59 sock_t irc_connect(const char *host, const char *port, const int tls, const char *ca_file) {
60 sock_t sock;
61 struct addrinfo *results, *r;
62 struct timeval timeout;
63
64 int err = getaddrinfo(host, port, NULL, &results); OR_DIE_gai(err); /*unable to resolve*/
65
66 timeout.tv_sec = 10;
67 timeout.tv_usec = 0;
68
69 for (r = results; r != NULL; r = r->ai_next) {
70 sock.fd = socket(r->ai_family, SOCK_STREAM, 0);
71 if (sock.fd < 0) continue;
72
73 setsockopt(sock.fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout) OR_DIE;
74 if (connect(sock.fd, r->ai_addr, r->ai_addrlen) == 0)
75 break; /* successfully connected */
76
77 close(sock.fd); /* failed, try next addr */
78 }
79
80 if (r == NULL) {
81 /* all failed; abort. */
82 sock.fd = -1;
83 } else {
84 /* connection established. */
85 if (tls != NO_TLS) {
86 struct tls *ctx = tls_client();
87 struct tls_config *cfg = tls_config_new();
88
89 if (tls == INSECURE_TLS) {
90 tls_config_insecure_noverifycert(cfg);
91 tls_config_insecure_noverifyname(cfg);
92 tls_config_insecure_noverifytime(cfg);
93 tls_config_set_ciphers(cfg, "legacy"); /* even more: 'insecure' */
94 }
95 tls_config_set_dheparams(cfg, "auto") OR_DIE_tls(ctx);
96 if (ca_file) tls_config_set_ca_file(cfg, ca_file) OR_DIE_tls(ctx);
97 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
98 /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */
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 /* TODO: our current handling of only a specific set of error numerics makes us vulnerable to hangs, when an unexpected code was received. we should really respect a response timeout here (during setup this must be quite large, because of ident and port scanning!) */
133 switch (command) {
134 case PING: seen |= PING * (strncmp(line, "PONG ", 5)==0); break;
135 case JOIN: seen |= JOIN * (strncmp(line, "JOIN ", 5)==0);
136 seen |= ERRS * (strncmp(line, "403 ", 4)==0);
137 seen |= ERRS * (strncmp(line, "405 ", 4)==0);
138 seen |= ERRS * (strncmp(line, "471 ", 4)==0);
139 seen |= ERRS * (strncmp(line, "473 ", 4)==0);
140 seen |= ERRS * (strncmp(line, "474 ", 4)==0);
141 seen |= ERRS * (strncmp(line, "475 ", 4)==0);
142 seen |= ERRS * (strncmp(line, "476 ", 4)==0);
143 seen |= ERRS * (strncmp(line, "477 ", 4)==0); break;
144 case NICK: seen |= NICK * (strncmp(line, "001 ", 4)==0);
145 seen |= ERRS * (strncmp(line, "432 ", 4)==0);
146 seen |= ERRS * (strncmp(line, "433 ", 4)==0);
147 seen |= ERRS * (strncmp(line, "436 ", 4)==0);
148 seen |= ERRS * (strncmp(line, "464 ", 4)==0);
149 seen |= ERRS * (strncmp(line, "902 ", 4)==0);
150 seen |= ERRS * (strncmp(line, "904 ", 4)==0); break;
151 }
152 /* look for common error numerics if any command was given */
153 if (command & (NICK|JOIN)) {
154 seen |= ERRS * (strncmp(line, "400 ", 4)==0);
155 seen |= ERRS * (strncmp(line, "421 ", 4)==0);
156 seen |= ERRS * (strncmp(line, "465 ", 4)==0);
157 }
158 /* always look for a fatal error */
159 if (strncmp(line, "ERROR ", 6)==0) seen |= ERRS;
160
161 if (seen & ERRS) {
162 fprintf(stderr, "IRC error: %s\n", line);
163 exit(1);
164 }
165
166 /* reply to pings: */
167 if (strncmp(line, "PING ", 5) == 0) {
168 int n = strlen(line);
169 int crlf = line[n+1] == '\n'; /* strtok only removes first delimeter */
170 line[1] = 'O'; /* PING :foo -> PONG :foo */
171 line[n] = crlf ? '\r' : '\n'; /* re-terminate after strtok */
172 WRITE(sock, line, n+crlf+1);
173 }
174 } while ((line = strtok_r(NULL, "\r\n", &saveptr)));
175
176 return seen;
177 }
178
179 int irc_base64(char *buf, int n) {
180 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
181 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
182 buf[n+1] = buf[n+2] = buf[l] = '\0';
183 for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) {
184 v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0;
185 buf[o+0] = b[v>>18 & 0x3f];
186 buf[o+1] = b[v>>12 & 0x3f];
187 buf[o+2] = (i+1<n)? b[v>>06 & 0x3f]:'=';
188 buf[o+3] = (i+2<n)? b[v>>00 & 0x3f]:'=';
189 }
190 return l;
191 }
192
193 int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) {
194 char buf[BUFSIZ];
195 int n;
196 struct pollfd fds[1];
197 fds[0].fd = sock.fd;
198 fds[0].events = POLLIN;
199
200 if (pass_type == SASL_PLAIN_PASSWD) {
201 n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n");
202 WRITE(sock, buf, n);
203 } else if (pass_type == SERVER_PASSWD) {
204 n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass);
205 WRITE(sock, buf, n);
206 }
207
208 n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick);
209 WRITE(sock, buf, n);
210 n = snprintf(buf, BUFSIZ, "USER %s 0 * :%s\r\n", nick, nick);
211 WRITE(sock, buf, n);
212
213 if (pass_type == SASL_PLAIN_PASSWD) {
214 /* TODO: assert strlen(pass) < 300 or abort */
215 /* should wait for 'CAP <nick|*> ACK :<...>' */
216 WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20);
217 /* server sends 'AUTHENTICATE +' */
218 /* split base64-output into 400 byte chunks; if last is exactly
219 400 bytes, send empty msg ('+') afterwards */
220 n = snprintf(buf, BUFSIZ, "AUTHENTICATE %s%c%s%c%s", nick, 0, nick, 0, pass);
221 n = irc_base64(buf+13, n-13)+13; /*13==strlen("AUTHENTICATE ")*/
222 n += snprintf(buf+n, BUFSIZ-n, "\r\n");
223 WRITE(sock, buf, n);
224 /* wait for response 900+903 (ok) or 902/904 (err) */
225 WRITE(sock, "CAP END\r\n", 9);
226 }
227
228 /* block until we get a RPL_WELCOME or an error: */
229 for (;;) {
230 if (poll(fds, 1, POLL_TIMEOUT)) {
231 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
232 if (n == 0) return -1; /* server closed connection */
233 write(outfd, buf, n);
234 n = irc_answer(sock, buf, NICK);
235 if (n & NICK) break;
236 else if (n & ERRS) return -1;
237 }
238 }
239
240 if (chan) {
241 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
242 WRITE(sock, buf, n);
243
244 /* block until we get a JOIN response or an error: */
245 /* todo: dedup this block with NICK/RPL_WELCOME */
246 for (;;) {
247 if (poll(fds, 1, POLL_TIMEOUT)) {
248 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
249 if (n == 0) return -1; /* server closed connection */
250 write(outfd, buf, n);
251 n = irc_answer(sock, buf, JOIN);
252 if (n & JOIN) break;
253 else if (n & ERRS) return -1;
254 }
255 }
256 }
257
258 return 0;
259 }
260
261 long irc_time() {
262 struct timespec t;
263 clock_gettime(CLOCK_MONOTONIC, &t) OR_DIE;
264 return t.tv_sec*1000 + t.tv_nsec / 1000000; /* milliseconds */
265 }
266
267 int irc_poll(const sock_t sock, const int infd, const int outfd) {
268 int n;
269 char buf[BUFSIZ];
270
271 int want_pong = 0;
272 long recv_ts = irc_time();
273
274 enum { IRC, CLI };
275 struct pollfd fds[2];
276 fds[IRC].fd = sock.fd;
277 fds[IRC].events = POLLIN;
278 fds[CLI].fd = infd;
279 fds[CLI].events = POLLIN;
280
281 for (;;) {
282 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
283
284 /* XXX: should handle EINTR, EAGAIN -> retry
285 should handle EPIPE and others -> exit */
286 /* todo: could check for fds[IRC].revents & (POLLERR|POLLHUP): tcp FIN or RST received (should already be covered by n==0) */
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:pPsSkh")) != -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 default: irc_help(argv[0], opt != 'h');
369 }
370 }
371
372 if (optind < argc) {
373 host = argv[optind++];
374 } else {
375 /* too few positional arguments */
376 fprintf(stderr, "missing HOST\n");
377 irc_help(argv[0], 1);
378 }
379 if (optind < argc) {
380 port = argv[optind++];
381 } else {
382 port = (tls == NO_TLS)
383 ? DEFAULT_PORT_TCP
384 : DEFAULT_PORT_TLS;
385 }
386 if (optind < argc) {
387 /* too many positional arguments */
388 fprintf(stderr, "too many args\n");
389 irc_help(argv[0], 1);
390 }
391
392 if (pass_type != NO_PASSWD && pass == NULL) {
393 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
394 exit(1);
395 }
396
397 memset(&act, 0, sizeof act);
398 act.sa_handler = irc_sighandler;
399 sigaction(SIGHUP, &act, NULL);
400 sigaction(SIGINT, &act, NULL);
401 sigaction(SIGTERM, &act, NULL);
402
403 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
404 irc_setup(sock, 1, nick, pass, pass_type, chan);
405 rv = irc_poll(sock, 0, 1);
406 irc_cleanup(sock);
407
408 return (rv < 0);
409 }
Imprint / Impressum