]>
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>
18 #define DEFAULT_TLS NO_TLS
19 #define DEFAULT_PORT_TCP "6667"
20 #define DEFAULT_PORT_TLS "6697"
22 #define POLL_TIMEOUT 100 /*ms*/
23 #define PING_INTERVAL 120000 /*ms*/
24 #define PONG_TIMEOUT 2000 /*ms*/
25 #define SETUP_TIMEOUT 15000 /*ms*/
28 #define STR(x) STR_(x)
29 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
30 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
31 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
46 int fd
; /* always contains the underlying file descriptor */
48 struct tls
* tls
; /* tls context, or NULL with plain socket */
52 #define _IMPLFN(fn, sock, buf, sz) ( \
54 ? tls_ ## fn(sock.tls, buf, sz) \
55 : fn(sock.fd, buf, sz) \
58 #define _IMPLFN(fn, sock, buf, sz) \
61 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
62 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
64 void irc_help ( const char * exe
, const int code
) {
65 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
69 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
71 struct addrinfo
* results
, * r
;
72 struct timeval timeout
;
74 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
79 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
80 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
81 if ( sock
. fd
< 0 ) continue ;
83 setsockopt ( sock
. fd
, SOL_SOCKET
, SO_SNDTIMEO
, & timeout
, sizeof timeout
) OR_DIE
;
84 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
85 break ; /* successfully connected */
87 close ( sock
. fd
); /* failed, try next addr */
91 /* all failed; abort. */
94 /* connection established. */
97 struct tls
* ctx
= tls_client ();
98 struct tls_config
* cfg
= tls_config_new ();
100 if ( tls
== INSECURE_TLS
) {
101 tls_config_insecure_noverifycert ( cfg
);
102 tls_config_insecure_noverifyname ( cfg
);
103 tls_config_insecure_noverifytime ( cfg
);
104 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
106 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
107 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
108 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
110 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
111 tls_config_free ( cfg
);
112 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
113 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
116 } else sock
. tls
= NULL
;
118 /* connect timeout here */
121 freeaddrinfo ( results
);
125 enum { /* requested command: */
132 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
133 unsigned int seen
= 0 ;
135 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
136 /*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.*/
138 /* skip over prefix (servername): */
140 while (* line
&& * line
++ != ' ' );
142 /* look for command responses or relevant error numerics: */
144 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
145 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
146 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
147 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
148 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
150 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
151 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
152 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
153 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
154 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
155 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
156 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
157 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
158 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
159 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
160 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
162 /* look for common error numerics if any command was given */
163 if ( command
& ( NICK
| JOIN
)) {
164 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
165 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
166 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
168 /* always look for a fatal error */
169 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
172 fprintf ( stderr
, "IRC error: %s \n " , line
);
176 /* reply to pings: */
177 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
178 int n
= strlen ( line
);
179 int crlf
= line
[ n
+ 1 ] == ' \n ' ; /* strtok only removes first delimeter */
180 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
181 line
[ n
] = crlf
? ' \r ' : ' \n ' ; /* re-terminate after strtok */
182 WRITE ( sock
, line
, n
+ crlf
+ 1 );
184 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
189 int irc_base64 ( char * buf
, int n
) {
190 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
191 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
192 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
193 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
194 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
195 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
196 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
197 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
198 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
203 long irc_time ( void ) {
205 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
206 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
209 int irc_wait ( const sock_t sock
, const int outfd
, int cmd
, char * buf
) {
211 long start
= irc_time ();
212 struct pollfd fds
[ 1 ];
214 fds
[ 0 ]. events
= POLLIN
;
216 for (;;) { /* note: reusing callee's buf */
217 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
218 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
219 if ( n
== 0 ) return - 1 ; /* server closed connection */
220 write ( outfd
, buf
, n
);
221 n
= irc_answer ( sock
, buf
, cmd
);
222 if ( n
& cmd
) return 0 ;
223 if ( irc_time () - start
> SETUP_TIMEOUT
) {
224 fprintf ( stderr
, "IRC setup timeout \n " );
231 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
235 if ( pass_type
== SASL_PLAIN_PASSWD
) {
236 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
238 } else if ( pass_type
== SERVER_PASSWD
) {
239 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
243 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
245 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
248 if ( pass_type
== SASL_PLAIN_PASSWD
) {
249 /* note: should assert strlen(pass) < 300 for spec compliance */
250 /* should wait for 'CAP <nick|*> ACK :<...>' */
251 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
252 /* server sends 'AUTHENTICATE +' */
253 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
254 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
255 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
257 /* wait for response 900+903 (ok) or 902/904 (err) */
258 WRITE ( sock
, "CAP END \r\n " , 9 );
261 /* block until we get a RPL_WELCOME or an error: */
262 n
= irc_wait ( sock
, outfd
, NICK
, buf
);
266 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
269 /* block until we get a JOIN response or an error: */
270 n
= irc_wait ( sock
, outfd
, JOIN
, buf
);
277 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
282 long recv_ts
= irc_time ();
285 struct pollfd fds
[ 2 ];
286 fds
[ IRC
]. fd
= sock
. fd
;
287 fds
[ IRC
]. events
= POLLIN
;
289 fds
[ CLI
]. events
= POLLIN
;
292 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
294 /* todo: should retry on EINTR, EAGAIN */
295 if ( fds
[ IRC
]. revents
& POLLIN
) {
296 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
297 if ( n
== 0 ) return - 1 ; /* server closed connection */
298 #ifndef DISABLELIBTLS
299 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
301 write ( outfd
, buf
, n
);
302 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
304 recv_ts
= irc_time ();
306 if ( fds
[ CLI
]. revents
& POLLIN
) {
307 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
308 if ( n
== 0 ) return 0 ; /* we closed connection */
309 n
= WRITE ( sock
, buf
, n
);
310 #ifndef DISABLELIBTLS
311 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
314 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
315 n
= WRITE ( sock
, buf
, n
);
316 #ifndef DISABLELIBTLS
317 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
321 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
322 /* pong timeout reached, abort. */
323 fprintf ( stderr
, "PONG timeout \n " );
325 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
326 /* haven't rx'd anything in a while, sending ping. */
327 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
333 void irc_cleanup ( const sock_t sock
) {
334 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
335 #ifndef DISABLELIBTLS
341 shutdown ( sock
. fd
, SHUT_RDWR
);
347 void irc_sighandler ( int signum
) {
357 int main ( int argc
, char ** argv
) {
363 int tls
= DEFAULT_TLS
;
364 char * ca_file
= NULL
;
365 int pass_type
= NO_PASSWD
;
368 struct sigaction act
;
372 pass
= getenv ( "IRC_PASSWD" );
373 ca_file
= getenv ( "IRC_CAFILE" );
375 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkh" )) != - 1 ) {
377 case 'n' : nick
= optarg
; break ;
378 case 'p' : pass_type
= SERVER_PASSWD
; break ;
379 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
380 case 'S' : tls
= NO_TLS
; break ;
381 #ifndef DISABLELIBTLS
382 case 's' : tls
= USE_TLS
; break ;
383 case 'k' : tls
= INSECURE_TLS
; break ;
385 case 'j' : chan
= optarg
; break ;
386 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
391 host
= argv
[ optind
++];
393 /* too few positional arguments */
394 fprintf ( stderr
, "missing HOST \n " );
395 irc_help ( argv
[ 0 ], 1 );
398 port
= argv
[ optind
++];
400 port
= ( tls
== NO_TLS
)
405 /* too many positional arguments */
406 fprintf ( stderr
, "too many args \n " );
407 irc_help ( argv
[ 0 ], 1 );
410 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
411 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
415 memset (& act
, 0 , sizeof act
);
416 act
. sa_handler
= irc_sighandler
;
417 sigaction ( SIGHUP
, & act
, NULL
);
418 sigaction ( SIGINT
, & act
, NULL
);
419 sigaction ( SIGTERM
, & act
, NULL
);
421 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
422 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
423 rv
= irc_poll ( sock
, 0 , 1 );