]>
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 30000 /*ms*/
27 #define SOCKET_TIMEOUT 5000 /*ms*/
30 #define STR(x) STR_(x)
31 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
32 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
33 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
48 int fd
; /* always contains the underlying file descriptor */
49 struct tls
* tls
; /* tls context, or NULL with plain socket */
51 #define _IMPLFN(fn, sock, buf, sz) ( \
53 ? tls_ ## fn(sock.tls, buf, sz) \
54 : fn(sock.fd, buf, sz) \
56 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
57 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
59 void irc_help ( const char * exe
, const int code
) {
60 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
64 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
66 struct addrinfo
* results
, * r
;
67 struct timeval timeout
;
69 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
71 timeout
. tv_sec
= SOCKET_TIMEOUT
/ 1000 ;
72 timeout
. tv_usec
= ( SOCKET_TIMEOUT
% 1000 ) * 1000000 ;
74 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
75 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
76 if ( sock
. fd
< 0 ) continue ;
78 setsockopt ( sock
. fd
, SOL_SOCKET
, SO_SNDTIMEO
, & timeout
, sizeof timeout
) OR_DIE
;
79 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
80 break ; /* successfully connected */
82 close ( sock
. fd
); /* failed, try next addr */
86 /* all failed; abort. */
89 /* connection established. */
91 struct tls
* ctx
= tls_client ();
92 struct tls_config
* cfg
= tls_config_new ();
94 if ( tls
== INSECURE_TLS
) {
95 tls_config_insecure_noverifycert ( cfg
);
96 tls_config_insecure_noverifyname ( cfg
);
97 tls_config_insecure_noverifytime ( cfg
);
98 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
100 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
101 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
102 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
104 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
105 tls_config_free ( cfg
);
106 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
107 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
110 } else sock
. tls
= NULL
;
111 /* connect timeout here */
114 freeaddrinfo ( results
);
118 enum { /* requested command: */
125 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
126 unsigned int seen
= 0 ;
128 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
129 /*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.*/
131 /* skip over prefix (servername): */
133 while (* line
&& * line
++ != ' ' );
135 /* look for command responses or relevant error numerics: */
137 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
138 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
139 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
143 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
144 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
145 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
146 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
147 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
148 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
150 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
151 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
152 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
153 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
155 /* look for common error numerics if any command was given */
156 if ( command
& ( NICK
| JOIN
)) {
157 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
158 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
159 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
161 /* always look for a fatal error */
162 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
165 fprintf ( stderr
, "IRC error: %s \n " , line
);
169 /* reply to pings: */
170 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
171 int n
= strlen ( line
);
172 int crlf
= line
[ n
+ 1 ] == ' \n ' ; /* strtok only removes first delimeter */
173 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
174 line
[ n
] = crlf
? ' \r ' : ' \n ' ; /* re-terminate after strtok */
175 WRITE ( sock
, line
, n
+ crlf
+ 1 );
177 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
182 int irc_base64 ( char * buf
, int n
) {
183 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
184 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
185 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
186 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
187 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
188 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
189 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
190 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
191 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
196 long irc_time ( void ) {
198 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
199 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
202 int irc_wait ( const sock_t sock
, const int outfd
, int cmd
, char * buf
) {
204 long start
= irc_time ();
205 struct pollfd fds
[ 1 ];
207 fds
[ 0 ]. events
= POLLIN
;
209 for (;;) { /* note: reusing callee's buf */
210 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
211 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
212 if ( n
== 0 ) return - 1 ; /* server closed connection */
213 write ( outfd
, buf
, n
);
214 n
= irc_answer ( sock
, buf
, cmd
);
215 if ( n
& cmd
) return 0 ;
216 if ( irc_time () - start
> SETUP_TIMEOUT
) {
217 fprintf ( stderr
, "IRC setup timeout \n " );
224 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
228 if ( pass_type
== SASL_PLAIN_PASSWD
) {
229 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
231 } else if ( pass_type
== SERVER_PASSWD
) {
232 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
236 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
238 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
241 if ( pass_type
== SASL_PLAIN_PASSWD
) {
242 /* note: should assert strlen(pass) < 300 for spec compliance */
243 /* should wait for 'CAP <nick|*> ACK :<...>' */
244 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
245 /* server sends 'AUTHENTICATE +' */
246 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
247 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
248 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
250 /* wait for response 900+903 (ok) or 902/904 (err) */
251 WRITE ( sock
, "CAP END \r\n " , 9 );
254 /* block until we get a RPL_WELCOME or an error: */
255 n
= irc_wait ( sock
, outfd
, NICK
, buf
);
259 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
262 /* block until we get a JOIN response or an error: */
263 n
= irc_wait ( sock
, outfd
, JOIN
, buf
);
270 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
275 long recv_ts
= irc_time ();
278 struct pollfd fds
[ 2 ];
279 fds
[ IRC
]. fd
= sock
. fd
;
280 fds
[ IRC
]. events
= POLLIN
;
282 fds
[ CLI
]. events
= POLLIN
;
285 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
287 /* todo: should retry on EINTR, EAGAIN */
288 if ( fds
[ IRC
]. revents
& POLLIN
) {
289 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
290 if ( n
== 0 ) return - 1 ; /* server closed connection */
291 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
292 write ( outfd
, buf
, n
);
293 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
295 recv_ts
= irc_time ();
297 if ( fds
[ CLI
]. revents
& POLLIN
) {
298 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
299 if ( n
== 0 ) return 0 ; /* we closed connection */
300 n
= WRITE ( sock
, buf
, n
);
301 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
303 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
304 n
= WRITE ( sock
, buf
, n
);
305 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
308 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
309 /* pong timeout reached, abort. */
310 fprintf ( stderr
, "PONG timeout \n " );
312 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
313 /* haven't rx'd anything in a while, sending ping. */
314 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
320 void irc_cleanup ( const sock_t sock
) {
321 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
326 shutdown ( sock
. fd
, SHUT_RDWR
);
332 void irc_sighandler ( int signum
) {
342 int main ( int argc
, char ** argv
) {
348 int tls
= DEFAULT_TLS
;
349 char * ca_file
= NULL
;
350 int pass_type
= NO_PASSWD
;
353 struct sigaction act
;
357 pass
= getenv ( "IRC_PASSWD" );
358 ca_file
= getenv ( "IRC_CAFILE" );
360 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkhV" )) != - 1 ) {
362 case 'n' : nick
= optarg
; break ;
363 case 'p' : pass_type
= SERVER_PASSWD
; break ;
364 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
365 case 's' : tls
= USE_TLS
; break ;
366 case 'S' : tls
= NO_TLS
; break ;
367 case 'k' : tls
= INSECURE_TLS
; break ;
368 case 'j' : chan
= optarg
; break ;
369 case 'V' : printf ( PROGNAME
" " VERSION
" \n " ); return 0 ;
370 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
375 host
= argv
[ optind
++];
377 /* too few positional arguments */
378 fprintf ( stderr
, "missing HOST \n " );
379 irc_help ( argv
[ 0 ], 1 );
382 port
= argv
[ optind
++];
384 port
= ( tls
== NO_TLS
)
389 /* too many positional arguments */
390 fprintf ( stderr
, "too many args \n " );
391 irc_help ( argv
[ 0 ], 1 );
394 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
395 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
399 memset (& act
, 0 , sizeof act
);
400 act
. sa_handler
= irc_sighandler
;
401 sigaction ( SIGHUP
, & act
, NULL
);
402 sigaction ( SIGINT
, & act
, NULL
);
403 sigaction ( SIGTERM
, & act
, NULL
);
405 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
406 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
407 rv
= irc_poll ( sock
, 0 , 1 );