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