]>
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_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
196 struct pollfd fds
[ 1 ];
198 fds
[ 0 ]. events
= POLLIN
;
200 if ( pass_type
== SASL_PLAIN_PASSWD
) {
201 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
203 } else if ( pass_type
== SERVER_PASSWD
) {
204 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
208 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
210 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
213 if ( pass_type
== SASL_PLAIN_PASSWD
) {
214 /* TODO: assert strlen(pass) < 300 or abort */
215 /* should wait for 'CAP <nick|*> ACK :<...>' */
216 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
217 /* server sends 'AUTHENTICATE +' */
218 /* split base64-output into 400 byte chunks; if last is exactly
219 400 bytes, send empty msg ('+') afterwards */
220 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
221 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
222 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
224 /* wait for response 900+903 (ok) or 902/904 (err) */
225 WRITE ( sock
, "CAP END \r\n " , 9 );
228 /* block until we get a RPL_WELCOME or an error: */
230 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
231 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
232 if ( n
== 0 ) return - 1 ; /* server closed connection */
233 write ( outfd
, buf
, n
);
234 n
= irc_answer ( sock
, buf
, NICK
);
236 else if ( n
& ERRS
) return - 1 ;
241 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
244 /* block until we get a JOIN response or an error: */
245 /* todo: dedup this block with NICK/RPL_WELCOME */
247 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
248 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
249 if ( n
== 0 ) return - 1 ; /* server closed connection */
250 write ( outfd
, buf
, n
);
251 n
= irc_answer ( sock
, buf
, JOIN
);
253 else if ( n
& ERRS
) return - 1 ;
263 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
264 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
267 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
272 long recv_ts
= irc_time ();
275 struct pollfd fds
[ 2 ];
276 fds
[ IRC
]. fd
= sock
. fd
;
277 fds
[ IRC
]. events
= POLLIN
;
279 fds
[ CLI
]. events
= POLLIN
;
282 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
284 /* XXX: should handle EINTR, EAGAIN -> retry
285 should handle EPIPE and others -> exit */
286 /* todo: could check for fds[IRC].revents & (POLLERR|POLLHUP): tcp FIN or RST received (should already be covered by n==0) */
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:pPsSkh" )) != - 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 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
373 host
= argv
[ optind
++];
375 /* too few positional arguments */
376 fprintf ( stderr
, "missing HOST \n " );
377 irc_help ( argv
[ 0 ], 1 );
380 port
= argv
[ optind
++];
382 port
= ( tls
== NO_TLS
)
387 /* too many positional arguments */
388 fprintf ( stderr
, "too many args \n " );
389 irc_help ( argv
[ 0 ], 1 );
392 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
393 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
397 memset (& act
, 0 , sizeof act
);
398 act
. sa_handler
= irc_sighandler
;
399 sigaction ( SIGHUP
, & act
, NULL
);
400 sigaction ( SIGINT
, & act
, NULL
);
401 sigaction ( SIGTERM
, & act
, NULL
);
403 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
404 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
405 rv
= irc_poll ( sock
, 0 , 1 );