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