]>
git.gir.st - ircpipe.git/blob - ircpipe.c
1 /* Copyright 2024 Tobias Girstmair <https://gir.st/>, GPLv3 licensed. */
2 #define _POSIX_C_SOURCE 200112L /* getopt(>=2), strtok_r(*), getaddrinfo(>=200112L), clock_gettime(>=199309L), sigaction(*) */
11 #include <sys/types.h>
12 #include <sys/socket.h>
17 #define PROGNAME "ircpipe"
20 #define DEFAULT_TLS NO_TLS
21 #define DEFAULT_PORT_TCP "6667"
22 #define DEFAULT_PORT_TLS "6697"
24 #define POLL_TIMEOUT 100 /*ms*/
25 #define PING_INTERVAL 120000 /*ms*/
26 #define PONG_TIMEOUT 2000 /*ms*/
27 #define SETUP_TIMEOUT 30000 /*ms*/
28 #define SOCKET_TIMEOUT 5000 /*ms*/
31 #define STR(x) STR_(x)
32 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
33 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
34 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
49 int fd
; /* always contains the underlying file descriptor */
50 struct tls
* tls
; /* tls context, or NULL with plain socket */
52 #define _IMPLFN(fn, sock, buf, sz) ( \
54 ? tls_ ## fn(sock.tls, buf, sz) \
55 : fn(sock.fd, buf, sz) \
57 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
58 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
60 void irc_help ( const char * exe
, const int code
) {
61 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
65 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
67 struct addrinfo
* results
, * r
;
68 struct timeval timeout
;
70 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
72 timeout
. tv_sec
= SOCKET_TIMEOUT
/ 1000 ;
73 timeout
. tv_usec
= ( SOCKET_TIMEOUT
% 1000 ) * 1000000 ;
75 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
76 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
77 if ( sock
. fd
< 0 ) continue ;
79 setsockopt ( sock
. fd
, SOL_SOCKET
, SO_SNDTIMEO
, & timeout
, sizeof timeout
) OR_DIE
;
80 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
81 break ; /* successfully connected */
83 close ( sock
. fd
); /* failed, try next addr */
87 /* all failed; abort. */
90 /* connection established. */
92 struct tls
* ctx
= tls_client ();
93 struct tls_config
* cfg
= tls_config_new ();
95 if ( tls
== INSECURE_TLS
) {
96 tls_config_insecure_noverifycert ( cfg
);
97 tls_config_insecure_noverifyname ( cfg
);
98 tls_config_insecure_noverifytime ( cfg
);
99 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
101 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
102 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
103 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
105 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
106 tls_config_free ( cfg
);
107 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
108 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
111 } else sock
. tls
= NULL
;
112 /* connect timeout here */
115 freeaddrinfo ( results
);
119 enum { /* requested command: */
126 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
127 unsigned int seen
= 0 ;
129 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
130 /*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.*/
132 /* skip over prefix (servername): */
134 while (* line
&& * line
++ != ' ' );
136 /* look for command responses or relevant error numerics: */
138 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
139 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
143 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
144 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
145 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
146 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
147 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
148 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
149 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
150 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
151 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
152 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
153 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
154 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
156 /* look for common error numerics if any command was given */
157 if ( command
& ( NICK
| JOIN
)) {
158 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
159 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
160 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
162 /* always look for a fatal error */
163 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
166 fprintf ( stderr
, "IRC error: %s \n " , line
);
170 /* reply to pings: */
171 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
172 int n
= strlen ( line
);
173 int crlf
= line
[ n
+ 1 ] == ' \n ' ; /* strtok only removes first delimeter */
174 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
175 line
[ n
] = crlf
? ' \r ' : ' \n ' ; /* re-terminate after strtok */
176 WRITE ( sock
, line
, n
+ crlf
+ 1 );
178 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
183 int irc_base64 ( char * buf
, int n
) {
184 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
185 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
186 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
187 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
188 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
189 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
190 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
191 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
192 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
197 long irc_time ( void ) {
199 clock_gettime ( CLOCK_MONOTONIC
, & t
) OR_DIE
;
200 return t
. tv_sec
* 1000 + t
. tv_nsec
/ 1000000 ; /* milliseconds */
203 int irc_wait ( const sock_t sock
, const int outfd
, int cmd
, char * buf
) {
205 long start
= irc_time ();
206 struct pollfd fds
[ 1 ];
208 fds
[ 0 ]. events
= POLLIN
;
210 for (;;) { /* note: reusing callee's buf */
211 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
212 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
213 if ( n
== 0 ) return - 1 ; /* server closed connection */
214 write ( outfd
, buf
, n
);
215 n
= irc_answer ( sock
, buf
, cmd
);
216 if ( n
& cmd
) return 0 ;
217 if ( irc_time () - start
> SETUP_TIMEOUT
) {
218 fprintf ( stderr
, "IRC setup timeout \n " );
225 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
229 if ( pass_type
== SASL_PLAIN_PASSWD
) {
230 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
232 } else if ( pass_type
== SERVER_PASSWD
) {
233 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
237 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
239 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
242 if ( pass_type
== SASL_PLAIN_PASSWD
) {
243 /* note: should assert strlen(pass) < 300 for spec compliance */
244 /* should wait for 'CAP <nick|*> ACK :<...>' */
245 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
246 /* server sends 'AUTHENTICATE +' */
247 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
248 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
249 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
251 /* wait for response 900+903 (ok) or 902/904 (err) */
252 WRITE ( sock
, "CAP END \r\n " , 9 );
255 /* block until we get a RPL_WELCOME or an error: */
256 n
= irc_wait ( sock
, outfd
, NICK
, buf
);
260 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
263 /* block until we get a JOIN response or an error: */
264 n
= irc_wait ( sock
, outfd
, JOIN
, buf
);
271 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
276 long recv_ts
= irc_time ();
279 struct pollfd fds
[ 2 ];
280 fds
[ IRC
]. fd
= sock
. fd
;
281 fds
[ IRC
]. events
= POLLIN
;
283 fds
[ CLI
]. events
= POLLIN
;
286 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
288 /* todo: should retry on EINTR, EAGAIN */
289 if ( fds
[ IRC
]. revents
& POLLIN
) {
290 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
291 if ( n
== 0 ) return - 1 ; /* server closed connection */
292 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
293 write ( outfd
, buf
, n
);
294 if ( irc_answer ( sock
, buf
, want_pong
? PING
: NO_CMD
) & PING
)
296 recv_ts
= irc_time ();
298 if ( fds
[ CLI
]. revents
& POLLIN
) {
299 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
300 if ( n
== 0 ) return 0 ; /* we closed connection */
301 n
= WRITE ( sock
, buf
, n
);
302 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
304 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
305 n
= WRITE ( sock
, buf
, n
);
306 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
309 if ( want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
+ PONG_TIMEOUT
) {
310 /* pong timeout reached, abort. */
311 fprintf ( stderr
, "PONG timeout \n " );
313 } else if (! want_pong
&& irc_time () - recv_ts
> PING_INTERVAL
) {
314 /* haven't rx'd anything in a while, sending ping. */
315 WRITE ( sock
, "PING :ircpipe \r\n " , 15 );
321 void irc_cleanup ( const sock_t sock
) {
322 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
327 shutdown ( sock
. fd
, SHUT_RDWR
);
333 void irc_sighandler ( int signum
) {
343 int main ( int argc
, char ** argv
) {
349 int tls
= DEFAULT_TLS
;
350 char * ca_file
= NULL
;
351 int pass_type
= NO_PASSWD
;
354 struct sigaction act
;
358 pass
= getenv ( "IRC_PASSWD" );
359 ca_file
= getenv ( "IRC_CAFILE" );
361 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkhV" )) != - 1 ) {
363 case 'n' : nick
= optarg
; break ;
364 case 'p' : pass_type
= SERVER_PASSWD
; break ;
365 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
366 case 's' : tls
= USE_TLS
; break ;
367 case 'S' : tls
= NO_TLS
; break ;
368 case 'k' : tls
= INSECURE_TLS
; break ;
369 case 'j' : chan
= optarg
; break ;
370 case 'V' : printf ( PROGNAME
" " VERSION
" \n " ); return 0 ;
371 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
376 host
= argv
[ optind
++];
378 /* too few positional arguments */
379 fprintf ( stderr
, "missing HOST \n " );
380 irc_help ( argv
[ 0 ], 1 );
383 port
= argv
[ optind
++];
385 port
= ( tls
== NO_TLS
)
390 /* too many positional arguments */
391 fprintf ( stderr
, "too many args \n " );
392 irc_help ( argv
[ 0 ], 1 );
395 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
396 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
400 memset (& act
, 0 , sizeof act
);
401 act
. sa_handler
= irc_sighandler
;
402 sigaction ( SIGHUP
, & act
, NULL
);
403 sigaction ( SIGINT
, & act
, NULL
);
404 sigaction ( SIGTERM
, & act
, NULL
);
406 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
407 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
408 rv
= irc_poll ( sock
, 0 , 1 );