]>
git.gir.st - ircpipe.git/blob - ircpipe.c
1 #define _POSIX_C_SOURCE 200809L /* getopt(>=2), dprintf(>=200809L), strtok_r(*), getaddrinfo(>=200112L) */
9 #include <sys/socket.h>
14 #define DEFAULT_PING 60000 /*ms*/
15 #define DEFAULT_TIMEOUT 2000 /*ms*/
16 #define DEFAULT_TLS NO_TLS
17 #define DEFAULT_PORT_TCP "6667"
18 #define DEFAULT_PORT_TLS "6697"
20 #define POLL_TIMEOUT 100
23 #define STR(x) STR_(x)
24 #define OR_DIE < 0 && (perror(__FILE__ ":" STR(__LINE__)), exit(1), 0)
25 #define OR_DIE_gai(err) if (err) {fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , gai_strerror(err));exit(1);}
26 #define OR_DIE_tls(ctx) < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , tls_error(ctx)), 1)), 0)
41 int fd
; /* always contains the underlying file descriptor */
42 struct tls
* tls
; /* tls context, or NULL with plain socket */
44 #define _IMPLFN(fn, sock, buf, sz) ( \
46 ? tls_ ## fn(sock.tls, buf, sz) \
47 : fn(sock.fd, buf, sz) \
49 #define READ(sock, buf, sz) _IMPLFN(read, sock, buf, sz)
50 #define WRITE(sock, buf, sz) _IMPLFN(write, sock, buf, sz)
52 void irc_help ( const char * exe
, const int code
) {
53 fprintf ( stderr
, "Usage: %s [-pP] [-sSk] [-n NICK] [-j CHAN] HOST [PORT] \n " , exe
);
57 sock_t
irc_connect ( const char * host
, const char * port
, const int tls
, const char * ca_file
) {
59 struct addrinfo
* results
, * r
;
61 int err
= getaddrinfo ( host
, port
, NULL
, & results
); OR_DIE_gai ( err
); /*unable to resolve*/
63 for ( r
= results
; r
!= NULL
; r
= r
-> ai_next
) {
64 sock
. fd
= socket ( r
-> ai_family
, SOCK_STREAM
, 0 );
65 if ( sock
. fd
< 0 ) continue ; /* try next; todo: should check errno */
67 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
68 break ; /* successfully connected */
70 close ( sock
. fd
); /* failed, try next addr */
74 /* all failed; abort. */
77 /* connection established. */
79 struct tls
* ctx
= tls_client ();
80 struct tls_config
* cfg
= tls_config_new ();
82 if ( tls
== INSECURE_TLS
) {
83 tls_config_insecure_noverifycert ( cfg
);
84 tls_config_insecure_noverifyname ( cfg
);
85 tls_config_insecure_noverifytime ( cfg
);
86 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
88 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
89 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
90 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
91 /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */
93 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
95 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
96 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
99 } else sock
. tls
= NULL
;
100 /* connect timeout here */
103 freeaddrinfo ( results
);
107 enum { /* requested command: */
114 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
115 unsigned int seen
= 0 ;
117 char * line
= strtok_r ( buf
, " \r\n " , & saveptr
);
118 /*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.*/
120 /* skip over prefix (servername): */
122 while (* line
&& * line
++ != ' ' );
124 /* look for command responses or error numerics, if required: */
126 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
127 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
128 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
129 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
130 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
131 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
132 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
133 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
134 seen
|= ERRS
* ( strncmp ( line
, "476 " , 4 )== 0 );
135 seen
|= ERRS
* ( strncmp ( line
, "477 " , 4 )== 0 ); break ;
136 case NICK
: seen
|= NICK
* ( strncmp ( line
, "001 " , 4 )== 0 );
137 seen
|= ERRS
* ( strncmp ( line
, "432 " , 4 )== 0 );
138 seen
|= ERRS
* ( strncmp ( line
, "433 " , 4 )== 0 );
139 seen
|= ERRS
* ( strncmp ( line
, "436 " , 4 )== 0 );
140 seen
|= ERRS
* ( strncmp ( line
, "464 " , 4 )== 0 );
141 seen
|= ERRS
* ( strncmp ( line
, "902 " , 4 )== 0 );
142 seen
|= ERRS
* ( strncmp ( line
, "904 " , 4 )== 0 ); break ;
144 /* look for common error numerics if any command was given */
145 if ( command
& ( NICK
| JOIN
)) {
146 seen
|= ERRS
* ( strncmp ( line
, "400 " , 4 )== 0 );
147 seen
|= ERRS
* ( strncmp ( line
, "421 " , 4 )== 0 );
148 seen
|= ERRS
* ( strncmp ( line
, "465 " , 4 )== 0 );
150 /* always look for a fatal error */
151 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
154 fprintf ( stderr
, __FILE__
":%d: %s \n " , __LINE__
, line
);
158 /* reply to pings: */
159 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
160 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
161 WRITE ( sock
, line
, strlen ( line
));
162 WRITE ( sock
, " \r\n " , 2 );
164 } while (( line
= strtok_r ( NULL
, " \r\n " , & saveptr
)));
169 int irc_base64 ( char * buf
, int n
) {
170 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
171 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
172 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
173 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
174 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
175 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
176 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
177 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
178 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
183 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
186 struct pollfd fds
[ 1 ];
188 fds
[ 0 ]. events
= POLLIN
;
190 if ( pass_type
== SASL_PLAIN_PASSWD
) {
191 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
193 } else if ( pass_type
== SERVER_PASSWD
) {
194 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
198 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
200 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
203 if ( pass_type
== SASL_PLAIN_PASSWD
) {
204 /* TODO: assert strlen(pass) < 300 or abort */
205 /* should wait for 'CAP <nick|*> ACK :<...>' */
206 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
207 /* server sends 'AUTHENTICATE +' */
208 /* split base64-output into 400 byte chunks; if last is exactly
209 400 bytes, send empty msg ('+') afterwards */
210 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE %s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
211 n
= irc_base64 ( buf
+ 13 , n
- 13 )+ 13 ; /*13==strlen("AUTHENTICATE ")*/
212 n
+= snprintf ( buf
+ n
, BUFSIZ
- n
, " \r\n " );
214 /* wait for response 900+903 (ok) or 902/904 (err) */
215 WRITE ( sock
, "CAP END \r\n " , 9 );
218 /* block until we get a RPL_WELCOME or an error: */
220 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
221 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
222 write ( outfd
, buf
, n
);
223 n
= irc_answer ( sock
, buf
, NICK
);
225 else if ( n
& ERRS
) return - 1 ;
230 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
233 /* block until we get a JOIN response or an error: */
234 /* todo: dedup this block with NICK/RPL_WELCOME */
236 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
237 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
238 write ( outfd
, buf
, n
);
239 n
= irc_answer ( sock
, buf
, JOIN
);
241 else if ( n
& ERRS
) return - 1 ;
249 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
253 struct pollfd fds
[ 2 ];
254 fds
[ IRC
]. fd
= sock
. fd
;
255 fds
[ IRC
]. events
= POLLIN
;
257 fds
[ CLI
]. events
= POLLIN
;
260 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
262 /* XXX: long responses don't get fully processed until user input */
263 /* XXX: must handle TLS_WANT_POLLIN and TLS_WANT_POLLOUT for READ and WRITE! */
264 if ( fds
[ IRC
]. revents
& POLLIN
) {
265 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
266 if ( n
== 0 ) return - 1 ; /* server closed connection */
267 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
268 write ( outfd
, buf
, n
);
269 irc_answer ( sock
, buf
, NO_CMD
);
270 /* update last-msg-rcvd here */
272 if ( fds
[ CLI
]. revents
& POLLIN
) {
273 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
274 if ( n
== 0 ) return 0 ; /* we closed connection */
275 n
= WRITE ( sock
, buf
, n
);
276 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
278 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
279 n
= WRITE ( sock
, buf
, n
);
280 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
282 /* TODO: if read/write on either irc or cli returns -1 and errno is EAGAIN or EINTR, retry. otherwise, return with error */
286 dprintf(sockfd, "PING\r\n");
288 if (irc_answer(sockfd, buf, NICK) & NICK) break;
293 void irc_cleanup ( const sock_t sock
) {
294 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
295 if ( sock
. tls
) tls_close ( sock
. tls
);
296 shutdown ( sock
. fd
, SHUT_RDWR
);
300 int main ( int argc
, char ** argv
) {
306 size_t ping_iv
= DEFAULT_PING
; /* interval between outgoing pings */
307 size_t resp_to
= DEFAULT_TIMEOUT
; /* how long to wait for command response (connect, ping, auth, ...) */
308 int tls
= DEFAULT_TLS
;
309 char * ca_file
= NULL
;
310 int pass_type
= NO_PASSWD
;
317 pass
= getenv ( "IRC_PASSWD" );
318 ca_file
= getenv ( "IRC_CAFILE" );
320 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkh" )) != - 1 ) {
322 case 'n' : nick
= optarg
; break ;
323 case 'p' : pass_type
= SERVER_PASSWD
; break ;
324 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
325 case 's' : tls
= USE_TLS
; break ;
326 case 'S' : tls
= NO_TLS
; break ;
327 case 'k' : tls
= INSECURE_TLS
; break ;
328 case 'j' : chan
= optarg
; break ;
329 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
334 host
= argv
[ optind
++];
336 /* too few positional arguments */
337 fprintf ( stderr
, "missing HOST \n " );
338 irc_help ( argv
[ 0 ], 1 );
341 port
= argv
[ optind
++];
343 port
= ( tls
== NO_TLS
)
348 /* too many positional arguments */
349 fprintf ( stderr
, "too many args \n " );
350 irc_help ( argv
[ 0 ], 1 );
353 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
354 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
358 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
359 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
);
360 rv
= irc_poll ( sock
, 0 , 1 );