]> git.gir.st - ircpipe.git/blob - ircpipe.c
implement TLS support using libtls or libretls
[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_PLAIN "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 [-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); /* default is 'none' */
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 = 1<<0,
109 NICK = 1<<1,
110 PING = 1<<2
111 };
112 int irc_answer(const sock_t sock, char *buf, const unsigned int command) {
113 unsigned int seen = 0;
114 char *saveptr;
115 char *line = strtok_r(buf, "\n", &saveptr);
116 do {
117 /* skip over prefix (servername): */
118 if (line[0] == ':')
119 while (*line++ != ' '); /*todo: check truncation(\0)*/
120
121 /* look for command responses, if any: */
122 switch (command) {
123 case PING: seen |= PING * (strncmp(line, "PONG ", 5)==0); break;
124 case NICK: seen |= NICK * (strncmp(line, "001 " , 4)==0); break;
125 }
126
127 /* reply to pings: */
128 if (strncmp(line, "PING ", 5) == 0) {
129 line[1] = 'O'; /* PING :foo -> PONG :foo */
130 WRITE(sock, line, strlen(line));
131 WRITE(sock, "\r\n", 2);
132 }
133 } while ((line = strtok_r(NULL, "\n", &saveptr)));
134
135 return seen;
136 }
137
138 int irc_base64(char *buf, int n) {
139 const char *b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
140 int i, o, v, l = ((n+(3-n%3)%3)/3)*4;
141 if (l >= 400) return -1; /* lazy */
142 buf[n+1] = buf[n+2] = buf[l] = '\0';
143 for (i=(n+(3-n%3)%3)-3, o=l-4; i>=0 && o>=0; i-=3, o-=4) {
144 v = buf[i+0]<<16 | buf[i+1]<<8 | buf[i+2]<<0;
145 buf[o+0] = b[v>>18 & 0x3f];
146 buf[o+1] = b[v>>12 & 0x3f];
147 buf[o+2] = (i+1<n)? b[v>>06 & 0x3f]:'=';
148 buf[o+3] = (i+2<n)? b[v>>00 & 0x3f]:'=';
149 }
150 return l;
151 }
152
153 int irc_setup(const sock_t sock, const int outfd, const char *nick, const char *pass, int pass_type, const char *chan) {
154 char buf[BUFSIZ];
155 int n;
156 struct pollfd fds[1];
157 fds[0].fd = sock.fd;
158 fds[0].events = POLLIN;
159
160 if (pass_type == SASL_PLAIN_PASSWD) {
161 n = snprintf(buf, BUFSIZ, "CAP REQ :sasl\r\n");
162 WRITE(sock, buf, n);
163 } else if (pass_type == SERVER_PASSWD) {
164 n = snprintf(buf, BUFSIZ, "PASS %s\r\n", pass);
165 WRITE(sock, buf, n);
166 }
167
168 n = snprintf(buf, BUFSIZ, "NICK %s\r\n", nick);
169 WRITE(sock, buf, n);
170 n = snprintf(buf, BUFSIZ, "USER %s 0.0.0.0 %s :%s\r\n", nick, nick, nick);
171 WRITE(sock, buf, n);
172
173 if (pass_type == SASL_PLAIN_PASSWD) {
174 int n2;
175 /* should wait for 'CAP <nick|*> ACK :<...>' */
176 WRITE(sock, "AUTHENTICATE PLAIN\r\n", 20);
177 /* server sends 'AUTHENTICATE +' */
178 /* split base64-output into 400 byte chunks; if last is exactly
179 400 bytes, send empty msg ('+') afterwards */
180 n = snprintf(buf, BUFSIZ, "AUTHENTICATE ");
181 n2 = snprintf(buf+n, BUFSIZ-n, "%s%c%s%c%s", nick, 0, nick, 0, pass);
182 irc_base64(buf+n, n2) OR_DIE;
183 snprintf(buf+n+n2, BUFSIZ-n-n2, "\r\n");
184 WRITE(sock, buf, n+n2+2);
185 /* wait for response 900+903 (ok) or 904 (err) */
186 WRITE(sock, "CAP END\r\n", 9);
187 }
188
189 /* block until we get a RPL_WELCOME: */
190 for (;;) {
191 if (poll(fds, 1, POLL_TIMEOUT)) {
192 n = READ(sock, buf, BUFSIZ);
193 write(outfd, buf, n);
194 if (irc_answer(sock, buf, NICK) & NICK) break;
195 /* todo: if SERVER_PASSWD is wrong, we get error code 464 instead of NICK/001*/
196 }
197 }
198
199 if (chan) {
200 n = snprintf(buf, BUFSIZ, "JOIN %s\r\n", chan);
201 WRITE(sock, buf, n);
202 }
203
204 return 0;
205 }
206
207 int irc_poll(const sock_t sock, const int infd, const int outfd) {
208 int n;
209 char buf[BUFSIZ];
210 enum { IRC, CLI };
211 struct pollfd fds[2];
212 fds[IRC].fd = sock.fd;
213 fds[IRC].events = POLLIN;
214 fds[CLI].fd = infd;
215 fds[CLI].events = POLLIN;
216
217 for (;;) {
218 poll(fds, 2, POLL_TIMEOUT) OR_DIE;
219
220 /* XXX: long responses don't get fully processed until user input */
221 /* XXX: must handle TLS_WANT_POLLIN and TLS_WANT_POLLOUT for READ and WRITE! */
222 if (fds[IRC].revents & POLLIN) {
223 n = READ(sock, buf, BUFSIZ);
224 if (n == 0) return -1; /* server closed connection */
225 write(outfd, buf, n);
226 irc_answer(sock, buf, NO_CMD);
227 /* update last-msg-rcvd here */
228 }
229 if (fds[CLI].revents & POLLIN) {
230 n = read(infd, buf, BUFSIZ);
231 if (n == 0) return 0; /* we closed connection */
232 WRITE(sock, buf, n);
233 }
234
235 /* send ping here */
236 /*
237 dprintf(sockfd, "PING\r\n");
238 // poll-read
239 if (irc_answer(sockfd, buf, NICK) & NICK) break;
240 */
241 }
242 }
243
244 void irc_cleanup(const sock_t sock) {
245 WRITE(sock, "QUIT :ircpipe\r\n", 15);
246 if (sock.tls) tls_close(sock.tls);
247 shutdown(sock.fd, SHUT_RDWR);
248 close(sock.fd);
249 }
250
251 int main(int argc, char **argv) {
252 char *host = NULL;
253 char *port = NULL;
254 char *nick = NULL;
255 char *pass = NULL;
256 char *chan = NULL;
257 size_t ping_iv = DEFAULT_PING; /* interval between outgoing pings */
258 size_t resp_to = DEFAULT_TIMEOUT; /* how long to wait for command response (connect, ping, auth, ...) */
259 int tls = DEFAULT_TLS;
260 char *ca_file = NULL;
261 int pass_type = NO_PASSWD;
262
263 sock_t sock;
264 int rv;
265
266 int opt; opterr = 0;
267
268 pass = getenv("IRC_PASSWD");
269 ca_file = getenv("IRC_CAFILE");
270
271 while ((opt = getopt(argc, argv, "n:j:pPsSkh")) != -1) {
272 switch (opt) {
273 case 'n': nick = optarg; break;
274 case 'p': pass_type = SERVER_PASSWD; break;
275 case 'P': pass_type = SASL_PLAIN_PASSWD; break;
276 case 's': tls = USE_TLS; break;
277 case 'S': tls = NO_TLS; break;
278 case 'k': tls = INSECURE_TLS; break;
279 case 'j': chan = optarg; break;
280 default: irc_help(argv[0], opt != 'h');
281 }
282 }
283
284 if (optind < argc) {
285 host = argv[optind++];
286 } else {
287 /* too few positional arguments */
288 fprintf(stderr, "missing HOST\n");
289 irc_help(argv[0], 1);
290 }
291 if (optind < argc) {
292 port = argv[optind++];
293 } else {
294 port = (tls == NO_TLS)
295 ? DEFAULT_PORT_PLAIN
296 : DEFAULT_PORT_TLS;
297 }
298 if (optind < argc) {
299 /* too many positional arguments */
300 fprintf(stderr, "too many args\n");
301 irc_help(argv[0], 1);
302 }
303
304 if (pass_type != NO_PASSWD && pass == NULL) {
305 fprintf(stderr, "must set IRC_PASSWD envvar to use -p/-P\n");
306 exit(1);
307 }
308
309 sock = irc_connect(host, port, tls, ca_file); sock.fd OR_DIE;
310 irc_setup(sock, 1, nick, pass, pass_type, chan) OR_DIE;
311 rv = irc_poll(sock, 0, 1);
312 irc_cleanup(sock);
313
314 return (rv < 0);
315 }
Imprint / Impressum