]>
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/socket.h>
15 #define DEFAULT_TLS NO_TLS
16 #define DEFAULT_PORT_TCP "6667"
17 #define DEFAULT_PORT_TLS "6697"
19 #define POLL_TIMEOUT 100
20 #define PING_INTERVAL 120000 /*ms*/
21 #define PONG_TIMEOUT 2000 /*ms*/
24 #define STR(x) STR_(x)
25 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
26 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
27 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
42 int fd
; /* always contains the underlying file descriptor */
43 struct tls
* tls
; /* tls context, or NULL with plain socket */
45 #define _IMPLFN(fn, sock, buf, sz) ( \
47 ? tls_ ## fn(sock.tls, buf, sz) \
48 : fn(sock.fd, buf, sz) \
50 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
51 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
53 void irc_help ( const char * exe
, const int code
) {
54 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
58 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
60 struct addrinfo
* results
, * r
;
62 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
64 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
65 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
66 if ( sock
. fd
< 0 ) continue ; /* try next; todo: should check errno */
68 /* TODO: should set socket timeout here */
69 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
70 break ; /* successfully connected */
72 close ( sock
. fd
); /* failed, try next addr */
76 /* all failed; abort. */
79 /* connection established. */
81 struct tls
* ctx
= tls_client ();
82 struct tls_config
* cfg
= tls_config_new ();
84 if ( tls
== INSECURE_TLS
) {
85 tls_config_insecure_noverifycert ( cfg
);
86 tls_config_insecure_noverifyname ( cfg
);
87 tls_config_insecure_noverifytime ( cfg
);
88 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
90 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
91 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
92 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
93 /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */
95 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
97 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
98 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
101 } else sock
. tls
= NULL
;
102 /* connect timeout here */
105 freeaddrinfo ( results
);
109 enum { /* requested command: */
116 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
117 unsigned int seen
= 0 ;
119 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
120 /*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.*/
122 /* skip over prefix (servername): */
124 while (* line
&& * line
++ != ' ' );
126 /* look for command responses or relevant error numerics: */
128 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
129 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
130 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
131 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
132 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
133 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
134 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
135 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
136 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
137 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
138 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
139 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
143 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
144 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
146 /* look for common error numerics if any command was given */
147 if ( command
& ( NICK
| JOIN
)) {
148 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
150 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
152 /* always look for a fatal error */
153 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
156 fprintf ( stderr
, __FILE__
":%d: %s \n " , __LINE__
, line
);
160 /* reply to pings: */
161 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
162 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
163 WRITE ( sock
, line
, strlen ( line
));
164 WRITE ( sock
, " \r\n " , 2 );
166 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
171 int irc_base64 ( char * buf
, int n
) {
172 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
173 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
174 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
175 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
176 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
177 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
178 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
179 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
180 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
185 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
188 struct pollfd fds
[ 1 ];
190 fds
[ 0 ]. events
= POLLIN
;
192 if ( pass_type
== SASL_PLAIN_PASSWD
) {
193 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
195 } else if ( pass_type
== SERVER_PASSWD
) {
196 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
200 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
202 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
205 if ( pass_type
== SASL_PLAIN_PASSWD
) {
206 /* TODO: assert strlen(pass) < 300 or abort */
207 /* should wait for 'CAP <nick|*> ACK :<...>' */
208 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
209 /* server sends 'AUTHENTICATE +' */
210 /* split base64-output into 400 byte chunks; if last is exactly
211 400 bytes, send empty msg ('+') afterwards */
212 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
213 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
214 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
216 /* wait for response 900+903 (ok) or 902/904 (err) */
217 WRITE ( sock
, "CAP END \r\n " , 9 );
220 /* block until we get a RPL_WELCOME or an error: */
222 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
223 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
224 write ( outfd
, buf
, n
);
225 n
= irc_answer ( sock
, buf
, NICK
);
227 else if ( n
& ERRS
) return - 1 ;
232 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
235 /* block until we get a JOIN response or an error: */
236 /* todo: dedup this block with NICK/RPL_WELCOME */
238 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
239 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
240 write ( outfd
, buf
, n
);
241 n
= irc_answer ( sock
, buf
, JOIN
);
243 else if ( n
& ERRS
) return - 1 ;
253 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
254 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
257 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
262 long recv_ts
= irc_time ();
265 struct pollfd fds
[ 2 ];
266 fds
[ IRC
]. fd
= sock
. fd
;
267 fds
[ IRC
]. events
= POLLIN
;
269 fds
[ CLI
]. events
= POLLIN
;
272 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
274 /* XXX: should handle EINTR, EAGAIN -> retry
275 should handle EPIPE and others -> exit */
276 /* todo: could check for fds[IRC].revents & (POLLERR|POLLHUP): tcp FIN or RST received */
277 if ( fds
[ IRC
]. revents
& POLLIN
) {
278 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
279 if ( n
== 0 ) return - 1 ; /* server closed connection */
280 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
281 write ( outfd
, buf
, n
);
282 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
284 recv_ts
= irc_time ();
286 if ( fds
[ CLI
]. revents
& POLLIN
) {
287 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
288 if ( n
== 0 ) return 0 ; /* we closed connection */
289 n
= WRITE ( sock
, buf
, n
);
290 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
292 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
293 n
= WRITE ( sock
, buf
, n
);
294 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
297 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
298 /* pong timeout reached, abort. */
299 fprintf ( stderr
, __FILE__
":%d: %s \n " , __LINE__
, "PONG timeout" );
301 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
302 /* haven't rx'd anything in a while, sending ping. */
303 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
309 void irc_cleanup ( const sock_t sock
) {
310 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
311 if ( sock
. tls
) tls_close ( sock
. tls
);
312 shutdown ( sock
. fd
, SHUT_RDWR
);
318 void irc_sighandler ( int signum
) {
328 int main ( int argc
, char ** argv
) {
334 int tls
= DEFAULT_TLS
;
335 char * ca_file
= NULL
;
336 int pass_type
= NO_PASSWD
;
339 struct sigaction act
;
343 pass
= getenv ( "IRC_PASSWD" );
344 ca_file
= getenv ( "IRC_CAFILE" );
346 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkh" )) != - 1 ) {
348 case 'n' : nick
= optarg
; break ;
349 case 'p' : pass_type
= SERVER_PASSWD
; break ;
350 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
351 case 's' : tls
= USE_TLS
; break ;
352 case 'S' : tls
= NO_TLS
; break ;
353 case 'k' : tls
= INSECURE_TLS
; break ;
354 case 'j' : chan
= optarg
; break ;
355 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
360 host
= argv
[ optind
++];
362 /* too few positional arguments */
363 fprintf ( stderr
, "missing HOST \n " );
364 irc_help ( argv
[ 0 ], 1 );
367 port
= argv
[ optind
++];
369 port
= ( tls
== NO_TLS
)
374 /* too many positional arguments */
375 fprintf ( stderr
, "too many args \n " );
376 irc_help ( argv
[ 0 ], 1 );
379 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
380 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
384 memset (& act
, 0 , sizeof act
);
385 act
. sa_handler
= irc_sighandler
;
386 sigaction ( SIGHUP
, & act
, NULL
);
387 sigaction ( SIGINT
, & act
, NULL
);
388 sigaction ( SIGTERM
, & act
, NULL
);
390 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
391 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
392 rv
= irc_poll ( sock
, 0 , 1 );