]> git.gir.st - ircpipe.git/blob - ircpipe.c
directly exit from irc_answer() on error, improve error handling
[ircpipe.git] / ircpipe.c
1 #define _POSIX_C_SOURCE 200809L /* getopt(>=2), dprintf(>=200809L), strtok_r(*), getaddrinfo(>=200112L) */
2 #include <netdb.h>
3 #include <poll.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_PING 60000 /*ms*/
15 #define DEFAULT_TIMEOUT 2000 /*ms*/
16 #define DEFAULT_TLS NO_TLS
17 #define DEFAULT_PORT_TCP "6667"
18 #define DEFAULT_PORT_TLS "6697"
19
20 #define POLL_TIMEOUT 100
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 if (connect(sock.fd, r->ai_addr, r->ai_addrlen) == 0)
68 break; /* successfully connected */
69
70 close(sock.fd); /* failed, try next addr */
71 }
72
73 if (r == NULL) {
74 /* all failed; abort. */
75 sock.fd = -1;
76 } else {
77 /* connection established. */
78 if (tls != NO_TLS) {
79 struct tls *ctx = tls_client();
80 struct tls_config *cfg = tls_config_new();
81
82 if (tls == INSECURE_TLS) {
83 tls_config_insecure_noverifycert(cfg);
84 tls_config_insecure_noverifyname(cfg);
85 tls_config_insecure_noverifytime(cfg);
86 tls_config_set_ciphers(cfg, "legacy"); /* even more: 'insecure' */
87 }
88 tls_config_set_dheparams(cfg, "auto") OR_DIE_tls(ctx);
89 if (ca_file) tls_config_set_ca_file(cfg, ca_file) OR_DIE_tls(ctx);
90 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
91 /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */
92
93 tls_configure(ctx, cfg) OR_DIE_tls(ctx);
94 tls_config_free(cfg);
95 tls_connect_socket(ctx, sock.fd, host) OR_DIE_tls(ctx);
96 tls_handshake(ctx) OR_DIE_tls(ctx);
97
98 sock.tls = ctx;
99 } else sock.tls = NULL;
100 /* connect timeout here */
101 }
102
103 freeaddrinfo(results);
104 return sock;
105 }
106
107 enum { /* requested command: */
108 NO_CMD = 0,
109 NICK = 1<<0,
110 JOIN = 1<<1,
111 PING = 1<<2,
112 ERRS = 1<<3
113 };
114 int irc_answer(const sock_t sock, char *buf, const unsigned int command) {
115 unsigned int seen = 0;
116 char *saveptr;
117 char *line = strtok_r(buf, "\r\n", &saveptr);
118 /*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.*/
119 do {
120 /* skip over prefix (servername): */
121 if (line[0] == ':')
122 while (*line && *line++ != ' ');
123
124 /* look for command responses or error numerics, if required: */
125 switch (command) {
126 case PING: seen |= PING * (strncmp(line, "PONG ", 5)==0); break;
127 case JOIN: seen |= JOIN * (strncmp(line, "JOIN ", 5)==0);
128 seen |= ERRS * (strncmp(line, "403 ", 4)==0);
129 seen |= ERRS * (strncmp(line, "405 ", 4)==0);
130 seen |= ERRS * (strncmp(line, "471 ", 4)==0);
131 seen |= ERRS * (strncmp(line, "473 ", 4)==0);
132 seen |= ERRS * (strncmp(line, "474 ", 4)==0);
133 seen |= ERRS * (strncmp(line, "475 ", 4)==0);
134 seen |= ERRS * (strncmp(line, "476 ", 4)==0);
135 seen |= ERRS * (strncmp(line, "477 ", 4)==0); break;
136 case NICK: seen |= NICK * (strncmp(line, "001 ", 4)==0);
137 seen |= ERRS * (strncmp(line, "432 ", 4)==0);
138 seen |= ERRS * (strncmp(line, "433 ", 4)==0);
139 seen |= ERRS * (strncmp(line, "436 ", 4)==0);
140 seen |= ERRS * (strncmp(line, "464 ", 4)==0);
141 seen |= ERRS * (strncmp(line, "902 ", 4)==0);
142 seen |= ERRS * (strncmp(line, "904 ", 4)==0); break;
143 }
144 /* look for common error numerics if any command was given */
145 if (command & (NICK|JOIN)) {
146 seen |= ERRS * (strncmp(line, "400 ", 4)==0);
147 seen |= ERRS * (strncmp(line, "421 ", 4)==0);
148 seen |= ERRS * (strncmp(line, "465 ", 4)==0);
149 }
150 /* always look for a fatal error */
151 if (strncmp(line, "ERROR ", 6)==0) seen |= ERRS;
152
153 if (seen & ERRS) {
154 fprintf(stderr, __FILE__ ":%d: %s\n", __LINE__, line);
155 exit(1);
156 }
157
158 /* reply to pings: */
159 if (strncmp(line, "PING ", 5) == 0) {
160 line[1] = 'O'; /* PING :foo -> PONG :foo */
161 WRITE(sock, line, strlen(line));
162 WRITE(sock, "\r\n", 2);
163 }
164 } while ((line = strtok_r(NULL, "\r\n", &saveptr)));
165
166 return seen;
167 }
168
169 int irc_base64(char *buf, int n) {
170 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
171 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
172 if (l >= 400) return -1; /* lazy */
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 int n2;
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 ");
212 n2 = snprintf(buf+n, BUFSIZ-n, "%s%c%s%c%s", nick, 0, nick, 0, pass);
213 irc_base64(buf+n, n2) OR_DIE;
214 snprintf(buf+n+n2, BUFSIZ-n-n2, "\r\n");
215 WRITE(sock, buf, n+n2+2);
216 /* wait for response 900+903 (ok) or 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 int irc_poll(const sock_t sock, const int infd, const int outfd) {
252 int n;
253 char buf[BUFSIZ];
254 enum { IRC, CLI };
255 struct pollfd fds[2];
256 fds[IRC].fd = sock.fd;
257 fds[IRC].events = POLLIN;
258 fds[CLI].fd = infd;
259 fds[CLI].events = POLLIN;
260
261 for (;;) {
262 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
263
264 /* XXX: long responses don't get fully processed until user input */
265 /* XXX: must handle TLS_WANT_POLLIN and TLS_WANT_POLLOUT for READ and WRITE! */
266 if (fds[IRC].revents & POLLIN) {
267 n = READ(sock, buf, BUFSIZ); buf[n] = '\0';
268 if (n == 0) return -1; /* server closed connection */
269 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
270 write(outfd, buf, n);
271 irc_answer(sock, buf, NO_CMD);
272 /* update last-msg-rcvd here */
273 }
274 if (fds[CLI].revents & POLLIN) {
275 n = read(infd, buf, BUFSIZ); buf[n] = '\0';
276 if (n == 0) return 0; /* we closed connection */
277 n = WRITE(sock, buf, n);
278 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
279 }
280 if (fds[IRC].revents & POLLOUT) { /* needed for TLS only */
281 n = WRITE(sock, buf, n);
282 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
283 }
284 /* TODO: if read/write on either irc or cli returns -1 and errno is EAGAIN or EINTR, retry. otherwise, return with error */
285
286 /* send ping here */
287 /*
288 dprintf(sockfd, "PING\r\n");
289 // poll-read
290 if (irc_answer(sockfd, buf, NICK) & NICK) break;
291 */
292 }
293 }
294
295 void irc_cleanup(const sock_t sock) {
296 WRITE(sock, "QUIT :ircpipe\r\n", 15);
297 if (sock.tls) tls_close(sock.tls);
298 shutdown(sock.fd, SHUT_RDWR);
299 close(sock.fd);
300 }
301
302 int main(int argc, char **argv) {
303 char *host = NULL;
304 char *port = NULL;
305 char *nick = NULL;
306 char *pass = NULL;
307 char *chan = NULL;
308 size_t ping_iv = DEFAULT_PING; /* interval between outgoing pings */
309 size_t resp_to = DEFAULT_TIMEOUT; /* how long to wait for command response (connect, ping, auth, ...) */
310 int tls = DEFAULT_TLS;
311 char *ca_file = NULL;
312 int pass_type = NO_PASSWD;
313
314 sock_t sock;
315 int rv;
316
317 int opt; opterr = 0;
318
319 pass = getenv("IRC_PASSWD");
320 ca_file = getenv("IRC_CAFILE");
321
322 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
323 switch (opt) {
324 case 'n': nick = optarg; break;
325 case 'p': pass_type = SERVER_PASSWD; break;
326 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
327 case 's': tls = USE_TLS; break;
328 case 'S': tls = NO_TLS; break;
329 case 'k': tls = INSECURE_TLS; break;
330 case 'j': chan = optarg; break;
331 default: irc_help(argv[0], opt != 'h');
332 }
333 }
334
335 if (optind < argc) {
336 host = argv[optind++];
337 } else {
338 /* too few positional arguments */
339 fprintf(stderr, "missing HOST\n");
340 irc_help(argv[0], 1);
341 }
342 if (optind < argc) {
343 port = argv[optind++];
344 } else {
345 port = (tls == NO_TLS)
346 ? DEFAULT_PORT_TCP
347 : DEFAULT_PORT_TLS;
348 }
349 if (optind < argc) {
350 /* too many positional arguments */
351 fprintf(stderr, "too many args\n");
352 irc_help(argv[0], 1);
353 }
354
355 if (pass_type != NO_PASSWD && pass == NULL) {
356 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
357 exit(1);
358 }
359
360 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
361 irc_setup(sock, 1, nick, pass, pass_type, chan);
362 rv = irc_poll(sock, 0, 1);
363 irc_cleanup(sock);
364
365 return (rv < 0);
366 }
Imprint / Impressum