]>
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 DEFAULT_TLS NO_TLS
17 #define DEFAULT_PORT_TCP "6667"
18 #define DEFAULT_PORT_TLS "6697"
20 #define POLL_TIMEOUT 100 /*ms*/
21 #define PING_INTERVAL 120000 /*ms*/
22 #define PONG_TIMEOUT 2000 /*ms*/
25 #define STR(x) STR_(x)
26 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
27 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
28 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
43 int fd
; /* always contains the underlying file descriptor */
44 struct tls
* tls
; /* tls context, or NULL with plain socket */
46 #define _IMPLFN(fn, sock, buf, sz) ( \
48 ? tls_ ## fn(sock.tls, buf, sz) \
49 : fn(sock.fd, buf, sz) \
51 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
52 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
54 void irc_help ( const char * exe
, const int code
) {
55 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
59 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
61 struct addrinfo
* results
, * r
;
62 struct timeval timeout
;
64 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
69 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
70 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
71 if ( sock
. fd
< 0 ) continue ;
73 setsockopt ( sock
. fd
, SOL_SOCKET
, SO_SNDTIMEO
, & timeout
, sizeof timeout
) OR_DIE
;
74 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
75 break ; /* successfully connected */
77 close ( sock
. fd
); /* failed, try next addr */
81 /* all failed; abort. */
84 /* connection established. */
86 struct tls
* ctx
= tls_client ();
87 struct tls_config
* cfg
= tls_config_new ();
89 if ( tls
== INSECURE_TLS
) {
90 tls_config_insecure_noverifycert ( cfg
);
91 tls_config_insecure_noverifyname ( cfg
);
92 tls_config_insecure_noverifytime ( cfg
);
93 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
95 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
96 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
97 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
99 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
100 tls_config_free ( cfg
);
101 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
102 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
105 } else sock
. tls
= NULL
;
106 /* connect timeout here */
109 freeaddrinfo ( results
);
113 enum { /* requested command: */
120 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
121 unsigned int seen
= 0 ;
123 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
124 /*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.*/
126 /* skip over prefix (servername): */
128 while (* line
&& * line
++ != ' ' );
130 /* look for command responses or relevant error numerics: */
131 /* TODO: our current handling of only a specific set of error numerics makes us vulnerable to hangs, when an unexpected code was received. we should really respect a response timeout here (during setup this must be quite large, because of ident and port scanning!) */
133 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
134 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
135 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
136 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
137 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
138 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
139 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
143 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
144 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
145 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
146 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
147 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
148 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
151 /* look for common error numerics if any command was given */
152 if ( command
& ( NICK
| JOIN
)) {
153 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
154 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
155 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
157 /* always look for a fatal error */
158 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
161 fprintf ( stderr
, "IRC error: %s \n " , line
);
165 /* reply to pings: */
166 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
167 int n
= strlen ( line
);
168 int crlf
= line
[ n
+ 1 ] == ' \n ' ; /* strtok only removes first delimeter */
169 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
170 line
[ n
] = crlf
? ' \r ' : ' \n ' ; /* re-terminate after strtok */
171 WRITE ( sock
, line
, n
+ crlf
+ 1 );
173 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
178 int irc_base64 ( char * buf
, int n
) {
179 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
180 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
181 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
182 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
183 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
184 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
185 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
186 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
187 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
192 int irc_wait ( const sock_t sock
, const int outfd
, int cmd
, char * buf
) {
194 struct pollfd fds
[ 1 ];
196 fds
[ 0 ]. events
= POLLIN
;
198 for (;;) { /* note: reusing callee's buf */
199 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
200 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
201 if ( n
== 0 ) return - 1 ; /* server closed connection */
202 write ( outfd
, buf
, n
);
203 n
= irc_answer ( sock
, buf
, cmd
);
204 if ( n
& cmd
) return 0 ;
205 else if ( n
& ERRS
) return - 1 ;
210 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
214 if ( pass_type
== SASL_PLAIN_PASSWD
) {
215 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
217 } else if ( pass_type
== SERVER_PASSWD
) {
218 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
222 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
224 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
227 if ( pass_type
== SASL_PLAIN_PASSWD
) {
228 /* note: should assert strlen(pass) < 300 for spec compliance */
229 /* should wait for 'CAP <nick|*> ACK :<...>' */
230 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
231 /* server sends 'AUTHENTICATE +' */
232 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
233 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
234 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
236 /* wait for response 900+903 (ok) or 902/904 (err) */
237 WRITE ( sock
, "CAP END \r\n " , 9 );
240 /* block until we get a RPL_WELCOME or an error: */
241 n
= irc_wait ( sock
, outfd
, NICK
, buf
);
245 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
248 /* block until we get a JOIN response or an error: */
249 n
= irc_wait ( sock
, outfd
, JOIN
, buf
);
258 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
259 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
262 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
267 long recv_ts
= irc_time ();
270 struct pollfd fds
[ 2 ];
271 fds
[ IRC
]. fd
= sock
. fd
;
272 fds
[ IRC
]. events
= POLLIN
;
274 fds
[ CLI
]. events
= POLLIN
;
277 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
279 /* todo: should retry on EINTR, EAGAIN */
280 if ( fds
[ IRC
]. revents
& POLLIN
) {
281 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
282 if ( n
== 0 ) return - 1 ; /* server closed connection */
283 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
284 write ( outfd
, buf
, n
);
285 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
287 recv_ts
= irc_time ();
289 if ( fds
[ CLI
]. revents
& POLLIN
) {
290 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
291 if ( n
== 0 ) return 0 ; /* we closed connection */
292 n
= WRITE ( sock
, buf
, n
);
293 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
295 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
296 n
= WRITE ( sock
, buf
, n
);
297 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
300 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
301 /* pong timeout reached, abort. */
302 fprintf ( stderr
, "PONG timeout \n " );
304 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
305 /* haven't rx'd anything in a while, sending ping. */
306 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
312 void irc_cleanup ( const sock_t sock
) {
313 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
318 shutdown ( sock
. fd
, SHUT_RDWR
);
324 void irc_sighandler ( int signum
) {
334 int main ( int argc
, char ** argv
) {
340 int tls
= DEFAULT_TLS
;
341 char * ca_file
= NULL
;
342 int pass_type
= NO_PASSWD
;
345 struct sigaction act
;
349 pass
= getenv ( "IRC_PASSWD" );
350 ca_file
= getenv ( "IRC_CAFILE" );
352 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkh" )) != - 1 ) {
354 case 'n' : nick
= optarg
; break ;
355 case 'p' : pass_type
= SERVER_PASSWD
; break ;
356 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
357 case 's' : tls
= USE_TLS
; break ;
358 case 'S' : tls
= NO_TLS
; break ;
359 case 'k' : tls
= INSECURE_TLS
; break ;
360 case 'j' : chan
= optarg
; break ;
361 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
366 host
= argv
[ optind
++];
368 /* too few positional arguments */
369 fprintf ( stderr
, "missing HOST \n " );
370 irc_help ( argv
[ 0 ], 1 );
373 port
= argv
[ optind
++];
375 port
= ( tls
== NO_TLS
)
380 /* too many positional arguments */
381 fprintf ( stderr
, "too many args \n " );
382 irc_help ( argv
[ 0 ], 1 );
385 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
386 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
390 memset (& act
, 0 , sizeof act
);
391 act
. sa_handler
= irc_sighandler
;
392 sigaction ( SIGHUP
, & act
, NULL
);
393 sigaction ( SIGINT
, & act
, NULL
);
394 sigaction ( SIGTERM
, & act
, NULL
);
396 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
397 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
398 rv
= irc_poll ( sock
, 0 , 1 );