]>
git.gir.st - ircpipe.git/blob - ircpipe.c
1 #define _POSIX_C_SOURCE 200112L /* getopt(>=2), strtok_r(*), getaddrinfo(>=200112L), clock_gettime(>=199309L), sigaction(*) */
10 #include <sys/types.h>
11 #include <sys/socket.h>
16 #define PROGNAME "ircpipe"
19 #define DEFAULT_TLS NO_TLS
20 #define DEFAULT_PORT_TCP "6667"
21 #define DEFAULT_PORT_TLS "6697"
23 #define POLL_TIMEOUT 100 /*ms*/
24 #define PING_INTERVAL 120000 /*ms*/
25 #define PONG_TIMEOUT 2000 /*ms*/
26 #define SETUP_TIMEOUT 15000 /*ms*/
29 #define STR(x) STR_(x)
30 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
31 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
32 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
47 int fd
; /* always contains the underlying file descriptor */
48 struct tls
* tls
; /* tls context, or NULL with plain socket */
50 #define _IMPLFN(fn, sock, buf, sz) ( \
52 ? tls_ ## fn(sock.tls, buf, sz) \
53 : fn(sock.fd, buf, sz) \
55 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
56 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
58 void irc_help ( const char * exe
, const int code
) {
59 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
63 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
65 struct addrinfo
* results
, * r
;
66 struct timeval timeout
;
68 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
73 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
74 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
75 if ( sock
. fd
< 0 ) continue ;
77 setsockopt ( sock
. fd
, SOL_SOCKET
, SO_SNDTIMEO
, & timeout
, sizeof timeout
) OR_DIE
;
78 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
79 break ; /* successfully connected */
81 close ( sock
. fd
); /* failed, try next addr */
85 /* all failed; abort. */
88 /* connection established. */
90 struct tls
* ctx
= tls_client ();
91 struct tls_config
* cfg
= tls_config_new ();
93 if ( tls
== INSECURE_TLS
) {
94 tls_config_insecure_noverifycert ( cfg
);
95 tls_config_insecure_noverifyname ( cfg
);
96 tls_config_insecure_noverifytime ( cfg
);
97 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
99 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
100 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
101 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
103 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
104 tls_config_free ( cfg
);
105 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
106 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
109 } else sock
. tls
= NULL
;
110 /* connect timeout here */
113 freeaddrinfo ( results
);
117 enum { /* requested command: */
124 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
125 unsigned int seen
= 0 ;
127 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
128 /*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.*/
130 /* skip over prefix (servername): */
132 while (* line
&& * line
++ != ' ' );
134 /* look for command responses or relevant error numerics: */
136 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
137 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
138 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
139 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
143 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
144 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
145 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
146 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
147 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
148 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
150 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
151 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
152 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
154 /* look for common error numerics if any command was given */
155 if ( command
& ( NICK
| JOIN
)) {
156 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
157 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
158 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
160 /* always look for a fatal error */
161 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
164 fprintf ( stderr
, "IRC error: %s \n " , line
);
168 /* reply to pings: */
169 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
170 int n
= strlen ( line
);
171 int crlf
= line
[ n
+ 1 ] == ' \n ' ; /* strtok only removes first delimeter */
172 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
173 line
[ n
] = crlf
? ' \r ' : ' \n ' ; /* re-terminate after strtok */
174 WRITE ( sock
, line
, n
+ crlf
+ 1 );
176 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
181 int irc_base64 ( char * buf
, int n
) {
182 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
183 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
184 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
185 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
186 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
187 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
188 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
189 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
190 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
195 long irc_time ( void ) {
197 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
198 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
201 int irc_wait ( const sock_t sock
, const int outfd
, int cmd
, char * buf
) {
203 long start
= irc_time ();
204 struct pollfd fds
[ 1 ];
206 fds
[ 0 ]. events
= POLLIN
;
208 for (;;) { /* note: reusing callee's buf */
209 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
210 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
211 if ( n
== 0 ) return - 1 ; /* server closed connection */
212 write ( outfd
, buf
, n
);
213 n
= irc_answer ( sock
, buf
, cmd
);
214 if ( n
& cmd
) return 0 ;
215 if ( irc_time () - start
> SETUP_TIMEOUT
) {
216 fprintf ( stderr
, "IRC setup timeout \n " );
223 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
227 if ( pass_type
== SASL_PLAIN_PASSWD
) {
228 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
230 } else if ( pass_type
== SERVER_PASSWD
) {
231 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
235 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
237 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
240 if ( pass_type
== SASL_PLAIN_PASSWD
) {
241 /* note: should assert strlen(pass) < 300 for spec compliance */
242 /* should wait for 'CAP <nick|*> ACK :<...>' */
243 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
244 /* server sends 'AUTHENTICATE +' */
245 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
246 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
247 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
249 /* wait for response 900+903 (ok) or 902/904 (err) */
250 WRITE ( sock
, "CAP END \r\n " , 9 );
253 /* block until we get a RPL_WELCOME or an error: */
254 n
= irc_wait ( sock
, outfd
, NICK
, buf
);
258 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
261 /* block until we get a JOIN response or an error: */
262 n
= irc_wait ( sock
, outfd
, JOIN
, buf
);
269 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
274 long recv_ts
= irc_time ();
277 struct pollfd fds
[ 2 ];
278 fds
[ IRC
]. fd
= sock
. fd
;
279 fds
[ IRC
]. events
= POLLIN
;
281 fds
[ CLI
]. events
= POLLIN
;
284 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
286 /* todo: should retry on EINTR, EAGAIN */
287 if ( fds
[ IRC
]. revents
& POLLIN
) {
288 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
289 if ( n
== 0 ) return - 1 ; /* server closed connection */
290 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
291 write ( outfd
, buf
, n
);
292 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
294 recv_ts
= irc_time ();
296 if ( fds
[ CLI
]. revents
& POLLIN
) {
297 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
298 if ( n
== 0 ) return 0 ; /* we closed connection */
299 n
= WRITE ( sock
, buf
, n
);
300 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
302 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
303 n
= WRITE ( sock
, buf
, n
);
304 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
307 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
308 /* pong timeout reached, abort. */
309 fprintf ( stderr
, "PONG timeout \n " );
311 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
312 /* haven't rx'd anything in a while, sending ping. */
313 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
319 void irc_cleanup ( const sock_t sock
) {
320 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
325 shutdown ( sock
. fd
, SHUT_RDWR
);
331 void irc_sighandler ( int signum
) {
341 int main ( int argc
, char ** argv
) {
347 int tls
= DEFAULT_TLS
;
348 char * ca_file
= NULL
;
349 int pass_type
= NO_PASSWD
;
352 struct sigaction act
;
356 pass
= getenv ( "IRC_PASSWD" );
357 ca_file
= getenv ( "IRC_CAFILE" );
359 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkhV" )) != - 1 ) {
361 case 'n' : nick
= optarg
; break ;
362 case 'p' : pass_type
= SERVER_PASSWD
; break ;
363 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
364 case 's' : tls
= USE_TLS
; break ;
365 case 'S' : tls
= NO_TLS
; break ;
366 case 'k' : tls
= INSECURE_TLS
; break ;
367 case 'j' : chan
= optarg
; break ;
368 case 'V' : printf ( PROGNAME
" " VERSION
" \n " ); return 0 ;
369 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
374 host
= argv
[ optind
++];
376 /* too few positional arguments */
377 fprintf ( stderr
, "missing HOST \n " );
378 irc_help ( argv
[ 0 ], 1 );
381 port
= argv
[ optind
++];
383 port
= ( tls
== NO_TLS
)
388 /* too many positional arguments */
389 fprintf ( stderr
, "too many args \n " );
390 irc_help ( argv
[ 0 ], 1 );
393 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
394 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
398 memset (& act
, 0 , sizeof act
);
399 act
. sa_handler
= irc_sighandler
;
400 sigaction ( SIGHUP
, & act
, NULL
);
401 sigaction ( SIGINT
, & act
, NULL
);
402 sigaction ( SIGTERM
, & act
, NULL
);
404 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
405 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
406 rv
= irc_poll ( sock
, 0 , 1 );