]> git.gir.st - ircpipe.git/blob - ircpipe.c
simplify error messages for non-syscalls
[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; /* try next; todo: should check errno */
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 line[1] = 'O'; /* PING :foo -> PONG :foo */
169 WRITE(sock, line, strlen(line));
170 WRITE(sock, "\r\n", 2);
171 }
172 } while ((line = strtok_r(NULL, "\r\n", &saveptr)));
173
174 return seen;
175 }
176
177 int irc_base64(char *buf, int n) {
178 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
179 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
180 buf[n+1] = buf[n+2] = buf[l] = '\0';
181 for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) {
182 v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0;
183 buf[o+0] = b[v>>18 & 0x3f];
184 buf[o+1] = b[v>>12 & 0x3f];
185 buf[o+2] = (i+1<n)? b[v>>06 & 0x3f]:'=';
186 buf[o+3] = (i+2<n)? b[v>>00 & 0x3f]:'=';
187 }
188 return l;
189 }
190
191 int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) {
192 char buf[BUFSIZ];
193 int n;
194 struct pollfd fds[1];
195 fds[0].fd = sock.fd;
196 fds[0].events = POLLIN;
197
198 if (pass_type == SASL_PLAIN_PASSWD) {
199 n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n");
200 WRITE(sock, buf, n);
201 } else if (pass_type == SERVER_PASSWD) {
202 n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass);
203 WRITE(sock, buf, n);
204 }
205
206 n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick);
207 WRITE(sock, buf, n);
208 n = snprintf(buf, BUFSIZ, "USER %s 0 * :%s\r\n", nick, nick);
209 WRITE(sock, buf, n);
210
211 if (pass_type == SASL_PLAIN_PASSWD) {
212 /* TODO: assert strlen(pass) < 300 or abort */
213 /* should wait for 'CAP <nick|*> ACK :<...>' */
214 WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20);
215 /* server sends 'AUTHENTICATE +' */
216 /* split base64-output into 400 byte chunks; if last is exactly
217 400 bytes, send empty msg ('+') afterwards */
218 n = snprintf(buf, BUFSIZ, "AUTHENTICATE %s%c%s%c%s", nick, 0, nick, 0, pass);
219 n = irc_base64(buf+13, n-13)+13; /*13==strlen("AUTHENTICATE ")*/
220 n += snprintf(buf+n, BUFSIZ-n, "\r\n");
221 WRITE(sock, buf, n);
222 /* wait for response 900+903 (ok) or 902/904 (err) */
223 WRITE(sock, "CAP END\r\n", 9);
224 }
225
226 /* block until we get a RPL_WELCOME or an error: */
227 for (;;) {
228 if (poll(fds, 1, POLL_TIMEOUT)) {
229 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
230 write(outfd, buf, n);
231 n = irc_answer(sock, buf, NICK);
232 if (n & NICK) break;
233 else if (n & ERRS) return -1;
234 }
235 }
236
237 if (chan) {
238 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
239 WRITE(sock, buf, n);
240
241 /* block until we get a JOIN response or an error: */
242 /* todo: dedup this block with NICK/RPL_WELCOME */
243 for (;;) {
244 if (poll(fds, 1, POLL_TIMEOUT)) {
245 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
246 write(outfd, buf, n);
247 n = irc_answer(sock, buf, JOIN);
248 if (n & JOIN) break;
249 else if (n & ERRS) return -1;
250 }
251 }
252 }
253
254 return 0;
255 }
256
257 long irc_time() {
258 struct timespec t;
259 clock_gettime(CLOCK_MONOTONIC, &t) OR_DIE;
260 return t.tv_sec*1000 + t.tv_nsec / 1000000; /* milliseconds */
261 }
262
263 int irc_poll(const sock_t sock, const int infd, const int outfd) {
264 int n;
265 char buf[BUFSIZ];
266
267 int want_pong = 0;
268 long recv_ts = irc_time();
269
270 enum { IRC, CLI };
271 struct pollfd fds[2];
272 fds[IRC].fd = sock.fd;
273 fds[IRC].events = POLLIN;
274 fds[CLI].fd = infd;
275 fds[CLI].events = POLLIN;
276
277 for (;;) {
278 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
279
280 /* XXX: should handle EINTR, EAGAIN -> retry
281 should handle EPIPE and others -> exit */
282 /* todo: could check for fds[IRC].revents & (POLLERR|POLLHUP): tcp FIN or RST received */
283 if (fds[IRC].revents & POLLIN) {
284 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
285 if (n == 0) return -1; /* server closed connection */
286 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
287 write(outfd, buf, n);
288 if (irc_answer(sock, buf, want_pong?PING:NO_CMD) & PING)
289 want_pong = 0;
290 recv_ts = irc_time();
291 }
292 if (fds[CLI].revents & POLLIN) {
293 n = read(infd, buf, BUFSIZ); buf[n] = '\0';
294 if (n == 0) return 0; /* we closed connection */
295 n = WRITE(sock, buf, n);
296 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
297 }
298 if (fds[IRC].revents & POLLOUT) { /* needed for TLS only */
299 n = WRITE(sock, buf, n);
300 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
301 }
302
303 if (want_pong && irc_time() - recv_ts > PING_INTERVAL + PONG_TIMEOUT) {
304 /* pong timeout reached, abort. */
305 fprintf(stderr, "PONG timeout");
306 return -1;
307 } else if (!want_pong && irc_time() - recv_ts > PING_INTERVAL) {
308 /* haven't rx'd anything in a while, sending ping. */
309 WRITE(sock, "PING :ircpipe\r\n", 15);
310 want_pong = 1;
311 }
312 }
313 }
314
315 void irc_cleanup(const sock_t sock) {
316 WRITE(sock, "QUIT :ircpipe\r\n", 15);
317 if (sock.tls) tls_close(sock.tls);
318 shutdown(sock.fd, SHUT_RDWR);
319 close(sock.fd);
320 }
321
322 sock_t sock;
323
324 void irc_sighandler(int signum) {
325 switch (signum) {
326 case SIGHUP:
327 case SIGINT:
328 case SIGTERM:
329 irc_cleanup(sock);
330 exit(signum);
331 }
332 }
333
334 int main(int argc, char **argv) {
335 char *host = NULL;
336 char *port = NULL;
337 char *nick = NULL;
338 char *pass = NULL;
339 char *chan = NULL;
340 int tls = DEFAULT_TLS;
341 char *ca_file = NULL;
342 int pass_type = NO_PASSWD;
343
344 int rv;
345 struct sigaction act;
346
347 int opt; opterr = 0;
348
349 pass = getenv("IRC_PASSWD");
350 ca_file = getenv("IRC_CAFILE");
351
352 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
353 switch (opt) {
354 case 'n': nick = optarg; break;
355 case 'p': pass_type = SERVER_PASSWD; break;
356 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
357 case 's': tls = USE_TLS; break;
358 case 'S': tls = NO_TLS; break;
359 case 'k': tls = INSECURE_TLS; break;
360 case 'j': chan = optarg; break;
361 default: irc_help(argv[0], opt != 'h');
362 }
363 }
364
365 if (optind < argc) {
366 host = argv[optind++];
367 } else {
368 /* too few positional arguments */
369 fprintf(stderr, "missing HOST\n");
370 irc_help(argv[0], 1);
371 }
372 if (optind < argc) {
373 port = argv[optind++];
374 } else {
375 port = (tls == NO_TLS)
376 ? DEFAULT_PORT_TCP
377 : DEFAULT_PORT_TLS;
378 }
379 if (optind < argc) {
380 /* too many positional arguments */
381 fprintf(stderr, "too many args\n");
382 irc_help(argv[0], 1);
383 }
384
385 if (pass_type != NO_PASSWD && pass == NULL) {
386 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
387 exit(1);
388 }
389
390 memset(&act, 0, sizeof act);
391 act.sa_handler = irc_sighandler;
392 sigaction(SIGHUP, &act, NULL);
393 sigaction(SIGINT, &act, NULL);
394 sigaction(SIGTERM, &act, NULL);
395
396 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
397 irc_setup(sock, 1, nick, pass, pass_type, chan);
398 rv = irc_poll(sock, 0, 1);
399 irc_cleanup(sock);
400
401 return (rv < 0);
402 }
Imprint / Impressum