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