]>
git.gir.st - ircpipe.git/blob - ircpipe.c
1 #define _POSIX_C_SOURCE 200809L /* getopt(>=2), dprintf(>=200809L), strtok_r(*), getaddrinfo(>=200112L) */
9 #include <sys/socket.h>
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"
20 #define POLL_TIMEOUT 100
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)
41 int fd
; /* always contains the underlying file descriptor */
42 struct tls
*tls
; /* tls context, or NULL with plain socket */
44 #define _IMPLFN(fn, sock, buf, sz) ( \
46 ? tls_ ## fn(sock.tls, buf, sz) \
47 : fn(sock.fd, buf, sz) \
49 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
50 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
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
);
57 sock_t
irc_connect(const char *host
, const char *port
, const int tls
, const char *ca_file
) {
59 struct addrinfo
*results
, *r
;
61 int err
= getaddrinfo(host
, port
, NULL
, &results
); OR_DIE_gai(err
); /*unable to resolve*/
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 */
67 if (connect(sock
.fd
, r
->ai_addr
, r
->ai_addrlen
) == 0)
68 break; /* successfully connected */
70 close(sock
.fd
); /* failed, try next addr */
74 /* all failed; abort. */
77 /* connection established. */
79 struct tls
*ctx
= tls_client();
80 struct tls_config
*cfg
= tls_config_new();
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' */
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) */
93 tls_configure(ctx
, cfg
) OR_DIE_tls(ctx
);
95 tls_connect_socket(ctx
, sock
.fd
, host
) OR_DIE_tls(ctx
);
96 tls_handshake(ctx
) OR_DIE_tls(ctx
);
99 } else sock
.tls
= NULL
;
100 /* connect timeout here */
103 freeaddrinfo(results
);
107 enum { /* requested command: */
112 int irc_answer(const sock_t sock
, char *buf
, const unsigned int command
) {
113 unsigned int seen
= 0;
115 char *line
= strtok_r(buf
, "\n", &saveptr
);
117 /* skip over prefix (servername): */
119 while (*line
&& *line
++ != ' ');
121 /* look for command responses, if any: */
123 case PING
: seen
|= PING
* (strncmp(line
, "PONG ", 5)==0); break;
124 case NICK
: seen
|= NICK
* (strncmp(line
, "001 " , 4)==0); break;
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);
133 } while ((line
= strtok_r(NULL
, "\n", &saveptr
)));
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]:'=';
153 int irc_setup(const sock_t sock
, const int outfd
, const char *nick
, const char *pass
, int pass_type
, const char *chan
) {
156 struct pollfd fds
[1];
158 fds
[0].events
= POLLIN
;
160 if (pass_type
== SASL_PLAIN_PASSWD
) {
161 n
= snprintf(buf
, BUFSIZ
, "CAP REQ :sasl\r\n");
163 } else if (pass_type
== SERVER_PASSWD
) {
164 n
= snprintf(buf
, BUFSIZ
, "PASS %s\r\n", pass
);
168 n
= snprintf(buf
, BUFSIZ
, "NICK %s\r\n", nick
);
170 n
= snprintf(buf
, BUFSIZ
, "USER %s 0 * :%s\r\n", nick
, nick
);
173 if (pass_type
== SASL_PLAIN_PASSWD
) {
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);
189 /* block until we get a RPL_WELCOME: */
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*/
200 n
= snprintf(buf
, BUFSIZ
, "JOIN %s\r\n", chan
);
207 int irc_poll(const sock_t sock
, const int infd
, const int outfd
) {
211 struct pollfd fds
[2];
212 fds
[IRC
].fd
= sock
.fd
;
213 fds
[IRC
].events
= POLLIN
;
215 fds
[CLI
].events
= POLLIN
;
218 poll(fds
, 2, POLL_TIMEOUT
) OR_DIE
;
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 fds
[IRC
].events
= POLLIN
| (n
==TLS_WANT_POLLOUT
?POLLOUT
:0);
226 write(outfd
, buf
, n
);
227 irc_answer(sock
, buf
, NO_CMD
);
228 /* update last-msg-rcvd here */
230 if (fds
[CLI
].revents
& POLLIN
) {
231 n
= read(infd
, buf
, BUFSIZ
);
232 if (n
== 0) return 0; /* we closed connection */
233 n
= WRITE(sock
, buf
, n
);
234 fds
[IRC
].events
= POLLIN
| (n
==TLS_WANT_POLLOUT
?POLLOUT
:0);
236 if (fds
[IRC
].revents
& POLLOUT
) { /* needed for TLS only */
237 n
= WRITE(sock
, buf
, n
);
238 fds
[IRC
].events
= POLLIN
| (n
==TLS_WANT_POLLOUT
?POLLOUT
:0);
240 /* TODO: if read/write on either irc or cli returns -1 and errno is EAGAIN or EINTR, retry. otherwise, return with error */
244 dprintf(sockfd, "PING\r\n");
246 if (irc_answer(sockfd, buf, NICK) & NICK) break;
251 void irc_cleanup(const sock_t sock
) {
252 WRITE(sock
, "QUIT :ircpipe\r\n", 15);
253 if (sock
.tls
) tls_close(sock
.tls
);
254 shutdown(sock
.fd
, SHUT_RDWR
);
258 int main(int argc
, char **argv
) {
264 size_t ping_iv
= DEFAULT_PING
; /* interval between outgoing pings */
265 size_t resp_to
= DEFAULT_TIMEOUT
; /* how long to wait for command response (connect, ping, auth, ...) */
266 int tls
= DEFAULT_TLS
;
267 char *ca_file
= NULL
;
268 int pass_type
= NO_PASSWD
;
275 pass
= getenv("IRC_PASSWD");
276 ca_file
= getenv("IRC_CAFILE");
278 while ((opt
= getopt(argc
, argv
, "n:j:pPsSkh")) != -1) {
280 case 'n': nick
= optarg
; break;
281 case 'p': pass_type
= SERVER_PASSWD
; break;
282 case 'P': pass_type
= SASL_PLAIN_PASSWD
; break;
283 case 's': tls
= USE_TLS
; break;
284 case 'S': tls
= NO_TLS
; break;
285 case 'k': tls
= INSECURE_TLS
; break;
286 case 'j': chan
= optarg
; break;
287 default: irc_help(argv
[0], opt
!= 'h');
292 host
= argv
[optind
++];
294 /* too few positional arguments */
295 fprintf(stderr
, "missing HOST\n");
296 irc_help(argv
[0], 1);
299 port
= argv
[optind
++];
301 port
= (tls
== NO_TLS
)
306 /* too many positional arguments */
307 fprintf(stderr
, "too many args\n");
308 irc_help(argv
[0], 1);
311 if (pass_type
!= NO_PASSWD
&& pass
== NULL
) {
312 fprintf(stderr
, "must set IRC_PASSWD envvar to use -p/-P\n");
316 sock
= irc_connect(host
, port
, tls
, ca_file
); sock
.fd OR_DIE
;
317 irc_setup(sock
, 1, nick
, pass
, pass_type
, chan
) OR_DIE
;
318 rv
= irc_poll(sock
, 0, 1);