]> git.gir.st - ircpipe.git/blob - ircpipe.c
improve PONG sending
[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 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 write(outfd, buf, n);
233 n = irc_answer(sock, buf, NICK);
234 if (n & NICK) break;
235 else if (n & ERRS) return -1;
236 }
237 }
238
239 if (chan) {
240 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
241 WRITE(sock, buf, n);
242
243 /* block until we get a JOIN response or an error: */
244 /* todo: dedup this block with NICK/RPL_WELCOME */
245 for (;;) {
246 if (poll(fds, 1, POLL_TIMEOUT)) {
247 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
248 write(outfd, buf, n);
249 n = irc_answer(sock, buf, JOIN);
250 if (n & JOIN) break;
251 else if (n & ERRS) return -1;
252 }
253 }
254 }
255
256 return 0;
257 }
258
259 long irc_time() {
260 struct timespec t;
261 clock_gettime(CLOCK_MONOTONIC, &t) OR_DIE;
262 return t.tv_sec*1000 + t.tv_nsec / 1000000; /* milliseconds */
263 }
264
265 int irc_poll(const sock_t sock, const int infd, const int outfd) {
266 int n;
267 char buf[BUFSIZ];
268
269 int want_pong = 0;
270 long recv_ts = irc_time();
271
272 enum { IRC, CLI };
273 struct pollfd fds[2];
274 fds[IRC].fd = sock.fd;
275 fds[IRC].events = POLLIN;
276 fds[CLI].fd = infd;
277 fds[CLI].events = POLLIN;
278
279 for (;;) {
280 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
281
282 /* XXX: should handle EINTR, EAGAIN -> retry
283 should handle EPIPE and others -> exit */
284 /* todo: could check for fds[IRC].revents & (POLLERR|POLLHUP): tcp FIN or RST received */
285 if (fds[IRC].revents & POLLIN) {
286 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
287 if (n == 0) return -1; /* server closed connection */
288 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
289 write(outfd, buf, n);
290 if (irc_answer(sock, buf, want_pong?PING:NO_CMD) & PING)
291 want_pong = 0;
292 recv_ts = irc_time();
293 }
294 if (fds[CLI].revents & POLLIN) {
295 n = read(infd, buf, BUFSIZ); buf[n] = '\0';
296 if (n == 0) return 0; /* we closed connection */
297 n = WRITE(sock, buf, n);
298 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
299 }
300 if (fds[IRC].revents & POLLOUT) { /* needed for TLS only */
301 n = WRITE(sock, buf, n);
302 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
303 }
304
305 if (want_pong && irc_time() - recv_ts > PING_INTERVAL + PONG_TIMEOUT) {
306 /* pong timeout reached, abort. */
307 fprintf(stderr, "PONG timeout");
308 return -1;
309 } else if (!want_pong && irc_time() - recv_ts > PING_INTERVAL) {
310 /* haven't rx'd anything in a while, sending ping. */
311 WRITE(sock, "PING :ircpipe\r\n", 15);
312 want_pong = 1;
313 }
314 }
315 }
316
317 void irc_cleanup(const sock_t sock) {
318 WRITE(sock, "QUIT :ircpipe\r\n", 15);
319 if (sock.tls) tls_close(sock.tls);
320 shutdown(sock.fd, SHUT_RDWR);
321 close(sock.fd);
322 }
323
324 sock_t sock;
325
326 void irc_sighandler(int signum) {
327 switch (signum) {
328 case SIGHUP:
329 case SIGINT:
330 case SIGTERM:
331 irc_cleanup(sock);
332 exit(signum);
333 }
334 }
335
336 int main(int argc, char **argv) {
337 char *host = NULL;
338 char *port = NULL;
339 char *nick = NULL;
340 char *pass = NULL;
341 char *chan = NULL;
342 int tls = DEFAULT_TLS;
343 char *ca_file = NULL;
344 int pass_type = NO_PASSWD;
345
346 int rv;
347 struct sigaction act;
348
349 int opt; opterr = 0;
350
351 pass = getenv("IRC_PASSWD");
352 ca_file = getenv("IRC_CAFILE");
353
354 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
355 switch (opt) {
356 case 'n': nick = optarg; break;
357 case 'p': pass_type = SERVER_PASSWD; break;
358 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
359 case 's': tls = USE_TLS; break;
360 case 'S': tls = NO_TLS; break;
361 case 'k': tls = INSECURE_TLS; break;
362 case 'j': chan = optarg; break;
363 default: irc_help(argv[0], opt != 'h');
364 }
365 }
366
367 if (optind < argc) {
368 host = argv[optind++];
369 } else {
370 /* too few positional arguments */
371 fprintf(stderr, "missing HOST\n");
372 irc_help(argv[0], 1);
373 }
374 if (optind < argc) {
375 port = argv[optind++];
376 } else {
377 port = (tls == NO_TLS)
378 ? DEFAULT_PORT_TCP
379 : DEFAULT_PORT_TLS;
380 }
381 if (optind < argc) {
382 /* too many positional arguments */
383 fprintf(stderr, "too many args\n");
384 irc_help(argv[0], 1);
385 }
386
387 if (pass_type != NO_PASSWD && pass == NULL) {
388 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
389 exit(1);
390 }
391
392 memset(&act, 0, sizeof act);
393 act.sa_handler = irc_sighandler;
394 sigaction(SIGHUP, &act, NULL);
395 sigaction(SIGINT, &act, NULL);
396 sigaction(SIGTERM, &act, NULL);
397
398 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
399 irc_setup(sock, 1, nick, pass, pass_type, chan);
400 rv = irc_poll(sock, 0, 1);
401 irc_cleanup(sock);
402
403 return (rv < 0);
404 }
Imprint / Impressum