]>
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 */
98 /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */
100 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
101 tls_config_free ( cfg
);
102 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
103 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
106 } else sock
. tls
= NULL
;
107 /* connect timeout here */
110 freeaddrinfo ( results
);
114 enum { /* requested command: */
121 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
122 unsigned int seen
= 0 ;
124 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
125 /*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.*/
127 /* skip over prefix (servername): */
129 while (* line
&& * line
++ != ' ' );
131 /* look for command responses or relevant error numerics: */
132 /* 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!) */
134 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
135 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
136 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
137 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
138 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
139 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
143 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
144 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
145 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
146 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
147 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
148 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
150 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
152 /* look for common error numerics if any command was given */
153 if ( command
& ( NICK
| JOIN
)) {
154 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
155 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
156 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
158 /* always look for a fatal error */
159 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
162 fprintf ( stderr
, "IRC error: %s \n " , line
);
166 /* reply to pings: */
167 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
168 int n
= strlen ( line
);
169 int crlf
= line
[ n
+ 1 ] == ' \n ' ; /* strtok only removes first delimeter */
170 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
171 line
[ n
] = crlf
? ' \r ' : ' \n ' ; /* re-terminate after strtok */
172 WRITE ( sock
, line
, n
+ crlf
+ 1 );
174 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
179 int irc_base64 ( char * buf
, int n
) {
180 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
181 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
182 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
183 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
184 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
185 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
186 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
187 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
188 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
193 int irc_wait ( const sock_t sock
, const int outfd
, int cmd
, char * buf
) {
195 struct pollfd fds
[ 1 ];
197 fds
[ 0 ]. events
= POLLIN
;
199 for (;;) { /* note: reusing callee's buf */
200 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
201 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
202 if ( n
== 0 ) return - 1 ; /* server closed connection */
203 write ( outfd
, buf
, n
);
204 n
= irc_answer ( sock
, buf
, cmd
);
205 if ( n
& cmd
) return 0 ;
206 else if ( n
& ERRS
) return - 1 ;
211 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
215 if ( pass_type
== SASL_PLAIN_PASSWD
) {
216 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
218 } else if ( pass_type
== SERVER_PASSWD
) {
219 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
223 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
225 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
228 if ( pass_type
== SASL_PLAIN_PASSWD
) {
229 /* TODO: assert strlen(pass) < 300 or abort */
230 /* should wait for 'CAP <nick|*> ACK :<...>' */
231 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
232 /* server sends 'AUTHENTICATE +' */
233 /* split base64-output into 400 byte chunks; if last is exactly
234 400 bytes, send empty msg ('+') afterwards */
235 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
236 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
237 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
239 /* wait for response 900+903 (ok) or 902/904 (err) */
240 WRITE ( sock
, "CAP END \r\n " , 9 );
243 /* block until we get a RPL_WELCOME or an error: */
244 n
= irc_wait ( sock
, outfd
, NICK
, buf
);
248 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
251 /* block until we get a JOIN response or an error: */
252 n
= irc_wait ( sock
, outfd
, JOIN
, buf
);
261 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
262 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
265 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
270 long recv_ts
= irc_time ();
273 struct pollfd fds
[ 2 ];
274 fds
[ IRC
]. fd
= sock
. fd
;
275 fds
[ IRC
]. events
= POLLIN
;
277 fds
[ CLI
]. events
= POLLIN
;
280 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
282 /* XXX: should handle EINTR, EAGAIN -> retry
283 should handle EPIPE and others -> exit */
284 /* todo: could check for fds[IRC].revents & (POLLERR|POLLHUP): tcp FIN or RST received (should already be covered by n==0) */
285 if ( fds
[ IRC
]. revents
& POLLIN
) {
286 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
287 if ( n
== 0 ) return - 1 ; /* server closed connection */
288 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
289 write ( outfd
, buf
, n
);
290 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
292 recv_ts
= irc_time ();
294 if ( fds
[ CLI
]. revents
& POLLIN
) {
295 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
296 if ( n
== 0 ) return 0 ; /* we closed connection */
297 n
= WRITE ( sock
, buf
, n
);
298 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
300 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
301 n
= WRITE ( sock
, buf
, n
);
302 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
305 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
306 /* pong timeout reached, abort. */
307 fprintf ( stderr
, "PONG timeout \n " );
309 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
310 /* haven't rx'd anything in a while, sending ping. */
311 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
317 void irc_cleanup ( const sock_t sock
) {
318 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
323 shutdown ( sock
. fd
, SHUT_RDWR
);
329 void irc_sighandler ( int signum
) {
339 int main ( int argc
, char ** argv
) {
345 int tls
= DEFAULT_TLS
;
346 char * ca_file
= NULL
;
347 int pass_type
= NO_PASSWD
;
350 struct sigaction act
;
354 pass
= getenv ( "IRC_PASSWD" );
355 ca_file
= getenv ( "IRC_CAFILE" );
357 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkh" )) != - 1 ) {
359 case 'n' : nick
= optarg
; break ;
360 case 'p' : pass_type
= SERVER_PASSWD
; break ;
361 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
362 case 's' : tls
= USE_TLS
; break ;
363 case 'S' : tls
= NO_TLS
; break ;
364 case 'k' : tls
= INSECURE_TLS
; break ;
365 case 'j' : chan
= optarg
; break ;
366 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
371 host
= argv
[ optind
++];
373 /* too few positional arguments */
374 fprintf ( stderr
, "missing HOST \n " );
375 irc_help ( argv
[ 0 ], 1 );
378 port
= argv
[ optind
++];
380 port
= ( tls
== NO_TLS
)
385 /* too many positional arguments */
386 fprintf ( stderr
, "too many args \n " );
387 irc_help ( argv
[ 0 ], 1 );
390 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
391 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
395 memset (& act
, 0 , sizeof act
);
396 act
. sa_handler
= irc_sighandler
;
397 sigaction ( SIGHUP
, & act
, NULL
);
398 sigaction ( SIGINT
, & act
, NULL
);
399 sigaction ( SIGTERM
, & act
, NULL
);
401 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
402 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
403 rv
= irc_poll ( sock
, 0 , 1 );