]> git.gir.st - ircpipe.git/blob - ircpipe.c
rework irc_answer to catch specific error replies and exit when encountered
[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 #define OR_DIE_irc < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s\n", "got IRC error"), 1)), 0)
28
29 enum pass_type_e {
30 NO_PASSWD,
31 SERVER_PASSWD,
32 SASL_PLAIN_PASSWD
33 };
34
35 enum tls_use_e {
36 NO_TLS,
37 USE_TLS,
38 INSECURE_TLS
39 };
40
41 typedef struct {
42 int fd; /* always contains the underlying file descriptor */
43 struct tls *tls; /* tls context, or NULL with plain socket */
44 } sock_t;
45 #define _IMPLFN(fn, sock, buf, sz) ( \
46 sock.tls \
47 ? tls_ ## fn(sock.tls, buf, sz) \
48 : fn(sock.fd, buf, sz) \
49 )
50 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
51 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
52
53 void irc_help(const char *exe, const int code) {
54 fprintf(stderr, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT]\n", exe);
55 exit(code);
56 }
57
58 sock_t irc_connect(const char *host, const char *port, const int tls, const char *ca_file) {
59 sock_t sock;
60 struct addrinfo *results, *r;
61
62 int err = getaddrinfo(host, port, NULL, &results); OR_DIE_gai(err); /*unable to resolve*/
63
64 for (r = results; r != NULL; r = r->ai_next) {
65 sock.fd = socket(r->ai_family, SOCK_STREAM, 0);
66 if (sock.fd < 0) continue; /* try next; todo: should check errno */
67
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, "\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 error numerics, if any: */
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); 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 fatal error, if any */
145 if (strncmp(line, "ERROR ", 6)==0) seen |= ERRS;
146
147 if (seen & ERRS) {
148 /* TODO: set &buf to line to preserve error across function call */
149 return seen;
150 }
151
152 /* reply to pings: */
153 if (strncmp(line, "PING ", 5) == 0) {
154 line[1] = 'O'; /* PING :foo -> PONG :foo */
155 WRITE(sock, line, strlen(line));
156 WRITE(sock, "\r\n", 2);
157 }
158 } while ((line = strtok_r(NULL, "\n", &saveptr)));
159
160 return seen;
161 }
162
163 int irc_base64(char *buf, int n) {
164 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
165 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
166 if (l >= 400) return -1; /* lazy */
167 buf[n+1] = buf[n+2] = buf[l] = '\0';
168 for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) {
169 v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0;
170 buf[o+0] = b[v>>18 & 0x3f];
171 buf[o+1] = b[v>>12 & 0x3f];
172 buf[o+2] = (i+1<n)? b[v>>06 & 0x3f]:'=';
173 buf[o+3] = (i+2<n)? b[v>>00 & 0x3f]:'=';
174 }
175 return l;
176 }
177
178 int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) {
179 char buf[BUFSIZ];
180 int n;
181 struct pollfd fds[1];
182 fds[0].fd = sock.fd;
183 fds[0].events = POLLIN;
184
185 if (pass_type == SASL_PLAIN_PASSWD) {
186 n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n");
187 WRITE(sock, buf, n);
188 } else if (pass_type == SERVER_PASSWD) {
189 n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass);
190 WRITE(sock, buf, n);
191 }
192
193 n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick);
194 WRITE(sock, buf, n);
195 n = snprintf(buf, BUFSIZ, "USER %s 0 * :%s\r\n", nick, nick);
196 WRITE(sock, buf, n);
197
198 if (pass_type == SASL_PLAIN_PASSWD) {
199 int n2;
200 /* should wait for 'CAP <nick|*> ACK :<...>' */
201 WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20);
202 /* server sends 'AUTHENTICATE +' */
203 /* split base64-output into 400 byte chunks; if last is exactly
204 400 bytes, send empty msg ('+') afterwards */
205 n = snprintf(buf, BUFSIZ, "AUTHENTICATE ");
206 n2 = snprintf(buf+n, BUFSIZ-n, "%s%c%s%c%s", nick, 0, nick, 0, pass);
207 irc_base64(buf+n, n2) OR_DIE;
208 snprintf(buf+n+n2, BUFSIZ-n-n2, "\r\n");
209 WRITE(sock, buf, n+n2+2);
210 /* wait for response 900+903 (ok) or 904 (err) */
211 WRITE(sock, "CAP END\r\n", 9);
212 }
213
214 /* block until we get a RPL_WELCOME or an error: */
215 for (;;) {
216 if (poll(fds, 1, POLL_TIMEOUT)) {
217 n = READ(sock, buf, BUFSIZ);
218 write(outfd, buf, n);
219 n = irc_answer(sock, buf, NICK);
220 if (n & NICK) break;
221 else if (n & ERRS) return -1; /* mostly for 433, 464 */
222 }
223 }
224
225 if (chan) {
226 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
227 WRITE(sock, buf, n);
228
229 /* block until we get a JOIN response or an error: */
230 /* todo: dedup this block with NICK/RPL_WELCOME */
231 for (;;) {
232 if (poll(fds, 1, POLL_TIMEOUT)) {
233 n = READ(sock, buf, BUFSIZ);
234 write(outfd, buf, n);
235 n = irc_answer(sock, buf, JOIN);
236 if (n & JOIN) break;
237 else if (n & ERRS) return -1; /* mostly for 403, 471-475 */
238 }
239 }
240 }
241
242 return 0;
243 }
244
245 int irc_poll(const sock_t sock, const int infd, const int outfd) {
246 int n;
247 char buf[BUFSIZ];
248 enum { IRC, CLI };
249 struct pollfd fds[2];
250 fds[IRC].fd = sock.fd;
251 fds[IRC].events = POLLIN;
252 fds[CLI].fd = infd;
253 fds[CLI].events = POLLIN;
254
255 for (;;) {
256 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
257
258 /* XXX: long responses don't get fully processed until user input */
259 /* XXX: must handle TLS_WANT_POLLIN and TLS_WANT_POLLOUT for READ and WRITE! */
260 if (fds[IRC].revents & POLLIN) {
261 n = READ(sock, buf, BUFSIZ);
262 if (n == 0) return -1; /* server closed connection */
263 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
264 write(outfd, buf, n);
265 irc_answer(sock, buf, NO_CMD);
266 /* update last-msg-rcvd here */
267 }
268 if (fds[CLI].revents & POLLIN) {
269 n = read(infd, buf, BUFSIZ);
270 if (n == 0) return 0; /* we closed connection */
271 n = WRITE(sock, buf, n);
272 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
273 }
274 if (fds[IRC].revents & POLLOUT) { /* needed for TLS only */
275 n = WRITE(sock, buf, n);
276 fds[IRC].events = POLLIN | (n==TLS_WANT_POLLOUT?POLLOUT:0);
277 }
278 /* TODO: if read/write on either irc or cli returns -1 and errno is EAGAIN or EINTR, retry. otherwise, return with error */
279
280 /* send ping here */
281 /*
282 dprintf(sockfd, "PING\r\n");
283 // poll-read
284 if (irc_answer(sockfd, buf, NICK) & NICK) break;
285 */
286 }
287 }
288
289 void irc_cleanup(const sock_t sock) {
290 WRITE(sock, "QUIT :ircpipe\r\n", 15);
291 if (sock.tls) tls_close(sock.tls);
292 shutdown(sock.fd, SHUT_RDWR);
293 close(sock.fd);
294 }
295
296 int main(int argc, char **argv) {
297 char *host = NULL;
298 char *port = NULL;
299 char *nick = NULL;
300 char *pass = NULL;
301 char *chan = NULL;
302 size_t ping_iv = DEFAULT_PING; /* interval between outgoing pings */
303 size_t resp_to = DEFAULT_TIMEOUT; /* how long to wait for command response (connect, ping, auth, ...) */
304 int tls = DEFAULT_TLS;
305 char *ca_file = NULL;
306 int pass_type = NO_PASSWD;
307
308 sock_t sock;
309 int rv;
310
311 int opt; opterr = 0;
312
313 pass = getenv("IRC_PASSWD");
314 ca_file = getenv("IRC_CAFILE");
315
316 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
317 switch (opt) {
318 case 'n': nick = optarg; break;
319 case 'p': pass_type = SERVER_PASSWD; break;
320 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
321 case 's': tls = USE_TLS; break;
322 case 'S': tls = NO_TLS; break;
323 case 'k': tls = INSECURE_TLS; break;
324 case 'j': chan = optarg; break;
325 default: irc_help(argv[0], opt != 'h');
326 }
327 }
328
329 if (optind < argc) {
330 host = argv[optind++];
331 } else {
332 /* too few positional arguments */
333 fprintf(stderr, "missing HOST\n");
334 irc_help(argv[0], 1);
335 }
336 if (optind < argc) {
337 port = argv[optind++];
338 } else {
339 port = (tls == NO_TLS)
340 ? DEFAULT_PORT_TCP
341 : DEFAULT_PORT_TLS;
342 }
343 if (optind < argc) {
344 /* too many positional arguments */
345 fprintf(stderr, "too many args\n");
346 irc_help(argv[0], 1);
347 }
348
349 if (pass_type != NO_PASSWD && pass == NULL) {
350 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
351 exit(1);
352 }
353
354 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
355 irc_setup(sock, 1, nick, pass, pass_type, chan) OR_DIE_irc;
356 rv = irc_poll(sock, 0, 1);
357 irc_cleanup(sock);
358
359 return (rv < 0);
360 }
Imprint / Impressum