]> git.gir.st - ircpipe.git/blob - ircpipe.c
add LF to error message
[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 /* 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\n");
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) {
320 tls_close(sock.tls);
321 tls_free(sock.tls);
322 }
323 shutdown(sock.fd, SHUT_RDWR);
324 close(sock.fd);
325 }
326
327 sock_t sock;
328
329 void irc_sighandler(int signum) {
330 switch (signum) {
331 case SIGHUP:
332 case SIGINT:
333 case SIGTERM:
334 irc_cleanup(sock);
335 exit(signum);
336 }
337 }
338
339 int main(int argc, char **argv) {
340 char *host = NULL;
341 char *port = NULL;
342 char *nick = NULL;
343 char *pass = NULL;
344 char *chan = NULL;
345 int tls = DEFAULT_TLS;
346 char *ca_file = NULL;
347 int pass_type = NO_PASSWD;
348
349 int rv;
350 struct sigaction act;
351
352 int opt; opterr = 0;
353
354 pass = getenv("IRC_PASSWD");
355 ca_file = getenv("IRC_CAFILE");
356
357 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
358 switch (opt) {
359 case 'n': nick = optarg; break;
360 case 'p': pass_type = SERVER_PASSWD; break;
361 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
362 case 's': tls = USE_TLS; break;
363 case 'S': tls = NO_TLS; break;
364 case 'k': tls = INSECURE_TLS; break;
365 case 'j': chan = optarg; break;
366 default: irc_help(argv[0], opt != 'h');
367 }
368 }
369
370 if (optind < argc) {
371 host = argv[optind++];
372 } else {
373 /* too few positional arguments */
374 fprintf(stderr, "missing HOST\n");
375 irc_help(argv[0], 1);
376 }
377 if (optind < argc) {
378 port = argv[optind++];
379 } else {
380 port = (tls == NO_TLS)
381 ? DEFAULT_PORT_TCP
382 : DEFAULT_PORT_TLS;
383 }
384 if (optind < argc) {
385 /* too many positional arguments */
386 fprintf(stderr, "too many args\n");
387 irc_help(argv[0], 1);
388 }
389
390 if (pass_type != NO_PASSWD && pass == NULL) {
391 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
392 exit(1);
393 }
394
395 memset(&act, 0, sizeof act);
396 act.sa_handler = irc_sighandler;
397 sigaction(SIGHUP, &act, NULL);
398 sigaction(SIGINT, &act, NULL);
399 sigaction(SIGTERM, &act, NULL);
400
401 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
402 irc_setup(sock, 1, nick, pass, pass_type, chan);
403 rv = irc_poll(sock, 0, 1);
404 irc_cleanup(sock);
405
406 return (rv < 0);
407 }
Imprint / Impressum