]> git.gir.st - ircpipe.git/blob - ircpipe.c
send all initialization commands at once
[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
99 tls_configure(ctx, cfg) OR_DIE_tls(ctx);
100 tls_config_free(cfg);
101 tls_connect_socket(ctx, sock.fd, host) OR_DIE_tls(ctx);
102 tls_handshake(ctx) OR_DIE_tls(ctx);
103
104 sock.tls = ctx;
105 } else sock.tls = NULL;
106 /* connect timeout here */
107 }
108
109 freeaddrinfo(results);
110 return sock;
111 }
112
113 enum { /* requested command: */
114 NO_CMD = 0,
115 NICK = 1<<0,
116 JOIN = 1<<1,
117 PING = 1<<2,
118 ERRS = 1<<3
119 };
120 int irc_answer(const sock_t sock, char *buf, const unsigned int command) {
121 unsigned int seen = 0;
122 char *saveptr;
123 char *line = strtok_r(buf, "\r\n", &saveptr);
124 /*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.*/
125 do {
126 /* skip over prefix (servername): */
127 if (line[0] == ':')
128 while (*line && *line++ != ' ');
129
130 /* look for command responses or relevant error numerics: */
131 /* 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!) */
132 switch (command) {
133 case PING: seen |= PING * (strncmp(line, "PONG ", 5)==0); break;
134 case JOIN: seen |= JOIN * (strncmp(line, "JOIN ", 5)==0);
135 seen |= ERRS * (strncmp(line, "403 ", 4)==0);
136 seen |= ERRS * (strncmp(line, "405 ", 4)==0);
137 seen |= ERRS * (strncmp(line, "471 ", 4)==0);
138 seen |= ERRS * (strncmp(line, "473 ", 4)==0);
139 seen |= ERRS * (strncmp(line, "474 ", 4)==0);
140 seen |= ERRS * (strncmp(line, "475 ", 4)==0);
141 seen |= ERRS * (strncmp(line, "476 ", 4)==0);
142 seen |= ERRS * (strncmp(line, "477 ", 4)==0); break;
143 case NICK: seen |= NICK * (strncmp(line, "001 ", 4)==0);
144 seen |= ERRS * (strncmp(line, "432 ", 4)==0);
145 seen |= ERRS * (strncmp(line, "433 ", 4)==0);
146 seen |= ERRS * (strncmp(line, "436 ", 4)==0);
147 seen |= ERRS * (strncmp(line, "464 ", 4)==0);
148 seen |= ERRS * (strncmp(line, "902 ", 4)==0);
149 seen |= ERRS * (strncmp(line, "904 ", 4)==0); break;
150 }
151 /* look for common error numerics if any command was given */
152 if (command & (NICK|JOIN)) {
153 seen |= ERRS * (strncmp(line, "400 ", 4)==0);
154 seen |= ERRS * (strncmp(line, "421 ", 4)==0);
155 seen |= ERRS * (strncmp(line, "465 ", 4)==0);
156 }
157 /* always look for a fatal error */
158 if (strncmp(line, "ERROR ", 6)==0) seen |= ERRS;
159
160 if (seen & ERRS) {
161 fprintf(stderr, "IRC error: %s\n", line);
162 exit(1);
163 }
164
165 /* reply to pings: */
166 if (strncmp(line, "PING ", 5) == 0) {
167 int n = strlen(line);
168 int crlf = line[n+1] == '\n'; /* strtok only removes first delimeter */
169 line[1] = 'O'; /* PING :foo -> PONG :foo */
170 line[n] = crlf ? '\r' : '\n'; /* re-terminate after strtok */
171 WRITE(sock, line, n+crlf+1);
172 }
173 } while ((line = strtok_r(NULL, "\r\n", &saveptr)));
174
175 return seen;
176 }
177
178 int irc_base64(char *buf, int n) {
179 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
180 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
181 buf[n+1] = buf[n+2] = buf[l] = '\0';
182 for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) {
183 v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0;
184 buf[o+0] = b[v>>18 & 0x3f];
185 buf[o+1] = b[v>>12 & 0x3f];
186 buf[o+2] = (i+1<n)? b[v>>06 & 0x3f]:'=';
187 buf[o+3] = (i+2<n)? b[v>>00 & 0x3f]:'=';
188 }
189 return l;
190 }
191
192 int irc_wait(const sock_t sock, const int outfd, int cmd, char* buf) {
193 int n;
194 struct pollfd fds[1];
195 fds[0].fd = sock.fd;
196 fds[0].events = POLLIN;
197
198 for (;;) { /* note: reusing callee's buf */
199 if (poll(fds, 1, POLL_TIMEOUT)) {
200 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
201 if (n == 0) return -1; /* server closed connection */
202 write(outfd, buf, n);
203 n = irc_answer(sock, buf, cmd);
204 if (n & cmd) return 0;
205 else if (n & ERRS) return -1;
206 }
207 }
208 }
209
210 int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) {
211 char buf[BUFSIZ];
212 int n;
213
214 if (pass_type == SASL_PLAIN_PASSWD) {
215 n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n");
216 WRITE(sock, buf, n);
217 } else if (pass_type == SERVER_PASSWD) {
218 n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass);
219 WRITE(sock, buf, n);
220 }
221
222 n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick);
223 WRITE(sock, buf, n);
224 n = snprintf(buf, BUFSIZ, "USER %s 0 * :%s\r\n", nick, nick);
225 WRITE(sock, buf, n);
226
227 if (pass_type == SASL_PLAIN_PASSWD) {
228 /* note: should assert strlen(pass) < 300 for spec compliance */
229 /* should wait for 'CAP <nick|*> ACK :<...>' */
230 WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20);
231 /* server sends 'AUTHENTICATE +' */
232 n = snprintf(buf, BUFSIZ, "AUTHENTICATE %s%c%s%c%s", nick, 0, nick, 0, pass);
233 n = irc_base64(buf+13, n-13)+13; /*13==strlen("AUTHENTICATE ")*/
234 n += snprintf(buf+n, BUFSIZ-n, "\r\n");
235 WRITE(sock, buf, n);
236 /* wait for response 900+903 (ok) or 902/904 (err) */
237 WRITE(sock, "CAP END\r\n", 9);
238 }
239
240 if (chan) {
241 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
242 WRITE(sock, buf, n);
243 /* block until we get a JOIN response or an error: */
244 n = irc_wait(sock, outfd, JOIN, buf);
245 } else {
246 /* block until we get a RPL_WELCOME or an error: */
247 n = irc_wait(sock, outfd, NICK, buf);
248 }
249
250 return n;
251 }
252
253 long irc_time() {
254 struct timespec t;
255 clock_gettime(CLOCK_MONOTONIC, &t) OR_DIE;
256 return t.tv_sec*1000 + t.tv_nsec / 1000000; /* milliseconds */
257 }
258
259 int irc_poll(const sock_t sock, const int infd, const int outfd) {
260 int n;
261 char buf[BUFSIZ];
262
263 int want_pong = 0;
264 long recv_ts = irc_time();
265
266 enum { IRC, CLI };
267 struct pollfd fds[2];
268 fds[IRC].fd = sock.fd;
269 fds[IRC].events = POLLIN;
270 fds[CLI].fd = infd;
271 fds[CLI].events = POLLIN;
272
273 for (;;) {
274 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
275
276 /* todo: should retry on EINTR, EAGAIN */
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, "PONG timeout\n");
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) {
312 tls_close(sock.tls);
313 tls_free(sock.tls);
314 }
315 shutdown(sock.fd, SHUT_RDWR);
316 close(sock.fd);
317 }
318
319 sock_t sock;
320
321 void irc_sighandler(int signum) {
322 switch (signum) {
323 case SIGHUP:
324 case SIGINT:
325 case SIGTERM:
326 irc_cleanup(sock);
327 exit(signum);
328 }
329 }
330
331 int main(int argc, char **argv) {
332 char *host = NULL;
333 char *port = NULL;
334 char *nick = NULL;
335 char *pass = NULL;
336 char *chan = NULL;
337 int tls = DEFAULT_TLS;
338 char *ca_file = NULL;
339 int pass_type = NO_PASSWD;
340
341 int rv;
342 struct sigaction act;
343
344 int opt; opterr = 0;
345
346 pass = getenv("IRC_PASSWD");
347 ca_file = getenv("IRC_CAFILE");
348
349 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
350 switch (opt) {
351 case 'n': nick = optarg; break;
352 case 'p': pass_type = SERVER_PASSWD; break;
353 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
354 case 's': tls = USE_TLS; break;
355 case 'S': tls = NO_TLS; break;
356 case 'k': tls = INSECURE_TLS; break;
357 case 'j': chan = optarg; break;
358 default: irc_help(argv[0], opt != 'h');
359 }
360 }
361
362 if (optind < argc) {
363 host = argv[optind++];
364 } else {
365 /* too few positional arguments */
366 fprintf(stderr, "missing HOST\n");
367 irc_help(argv[0], 1);
368 }
369 if (optind < argc) {
370 port = argv[optind++];
371 } else {
372 port = (tls == NO_TLS)
373 ? DEFAULT_PORT_TCP
374 : DEFAULT_PORT_TLS;
375 }
376 if (optind < argc) {
377 /* too many positional arguments */
378 fprintf(stderr, "too many args\n");
379 irc_help(argv[0], 1);
380 }
381
382 if (pass_type != NO_PASSWD && pass == NULL) {
383 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
384 exit(1);
385 }
386
387 memset(&act, 0, sizeof act);
388 act.sa_handler = irc_sighandler;
389 sigaction(SIGHUP, &act, NULL);
390 sigaction(SIGINT, &act, NULL);
391 sigaction(SIGTERM, &act, NULL);
392
393 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
394 irc_setup(sock, 1, nick, pass, pass_type, chan);
395 rv = irc_poll(sock, 0, 1);
396 irc_cleanup(sock);
397
398 return (rv < 0);
399 }
Imprint / Impressum