]>
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)
27 #define OR_DIE_irc < 0 && (exit((fprintf(stderr, __FILE__ ":" STR(__LINE__) ": %s \n " , "got IRC error" ), 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 if ( connect ( sock
. fd
, r
-> ai_addr
, r
-> ai_addrlen
) == 0 )
69 break ; /* successfully connected */
71 close ( sock
. fd
); /* failed, try next addr */
75 /* all failed; abort. */
78 /* connection established. */
80 struct tls
* ctx
= tls_client ();
81 struct tls_config
* cfg
= tls_config_new ();
83 if ( tls
== INSECURE_TLS
) {
84 tls_config_insecure_noverifycert ( cfg
);
85 tls_config_insecure_noverifyname ( cfg
);
86 tls_config_insecure_noverifytime ( cfg
);
87 tls_config_set_ciphers ( cfg
, "legacy" ); /* even more: 'insecure' */
89 tls_config_set_dheparams ( cfg
, "auto" ) OR_DIE_tls ( ctx
);
90 if ( ca_file
) tls_config_set_ca_file ( cfg
, ca_file
) OR_DIE_tls ( ctx
);
91 /* todo: if ca_file ends in /, call tls_config_set_ca_path() instead */
92 /* todo: otherwise, set to tls_default_ca_cert_file() iff libtls (not libretls) */
94 tls_configure ( ctx
, cfg
) OR_DIE_tls ( ctx
);
96 tls_connect_socket ( ctx
, sock
. fd
, host
) OR_DIE_tls ( ctx
);
97 tls_handshake ( ctx
) OR_DIE_tls ( ctx
);
100 } else sock
. tls
= NULL
;
101 /* connect timeout here */
104 freeaddrinfo ( results
);
108 enum { /* requested command: */
115 int irc_answer ( const sock_t sock
, char * buf
, const unsigned int command
) {
116 unsigned int seen
= 0 ;
118 char * line
= strtok_r ( buf
, " \n " , & saveptr
);
119 /*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.*/
121 /* skip over prefix (servername): */
123 while (* line
&& * line
++ != ' ' );
125 /* look for command responses or error numerics, if any: */
127 case PING
: seen
|= PING
* ( strncmp ( line
, "PONG " , 5 )== 0 ); break ;
128 case JOIN
: seen
|= JOIN
* ( strncmp ( line
, "JOIN " , 5 )== 0 );
129 seen
|= ERRS
* ( strncmp ( line
, "403 " , 4 )== 0 );
130 seen
|= ERRS
* ( strncmp ( line
, "405 " , 4 )== 0 );
131 seen
|= ERRS
* ( strncmp ( line
, "471 " , 4 )== 0 );
132 seen
|= ERRS
* ( strncmp ( line
, "473 " , 4 )== 0 );
133 seen
|= ERRS
* ( strncmp ( line
, "474 " , 4 )== 0 );
134 seen
|= ERRS
* ( strncmp ( line
, "475 " , 4 )== 0 );
135 seen
|= ERRS
* ( strncmp ( line
, "476 " , 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 fatal error, if any */
145 if ( strncmp ( line
, "ERROR " , 6 )== 0 ) seen
|= ERRS
;
148 /* TODO: set &buf to line to preserve error across function call */
152 /* reply to pings: */
153 if ( strncmp ( line
, "PING " , 5 ) == 0 ) {
154 line
[ 1 ] = 'O' ; /* PING :foo -> PONG :foo */
155 WRITE ( sock
, line
, strlen ( line
));
156 WRITE ( sock
, " \r\n " , 2 );
158 } while (( line
= strtok_r ( NULL
, " \n " , & saveptr
)));
163 int irc_base64 ( char * buf
, int n
) {
164 const char * b
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
165 int i
, o
, v
, l
= (( n
+( 3 - n
% 3 )% 3 )/ 3 )* 4 ;
166 if ( l
>= 400 ) return - 1 ; /* lazy */
167 buf
[ n
+ 1 ] = buf
[ n
+ 2 ] = buf
[ l
] = '\0' ;
168 for ( i
=( n
+( 3 - n
% 3 )% 3 )- 3 , o
= l
- 4 ; i
>= 0 && o
>= 0 ; i
-= 3 , o
-= 4 ) {
169 v
= buf
[ i
+ 0 ]<< 16 | buf
[ i
+ 1 ]<< 8 | buf
[ i
+ 2 ]<< 0 ;
170 buf
[ o
+ 0 ] = b
[ v
>> 18 & 0x3f ];
171 buf
[ o
+ 1 ] = b
[ v
>> 12 & 0x3f ];
172 buf
[ o
+ 2 ] = ( i
+ 1 < n
)? b
[ v
>> 06 & 0x3f ]: '=' ;
173 buf
[ o
+ 3 ] = ( i
+ 2 < n
)? b
[ v
>> 00 & 0x3f ]: '=' ;
178 int irc_setup ( const sock_t sock
, const int outfd
, const char * nick
, const char * pass
, int pass_type
, const char * chan
) {
181 struct pollfd fds
[ 1 ];
183 fds
[ 0 ]. events
= POLLIN
;
185 if ( pass_type
== SASL_PLAIN_PASSWD
) {
186 n
= snprintf ( buf
, BUFSIZ
, "CAP REQ :sasl \r\n " );
188 } else if ( pass_type
== SERVER_PASSWD
) {
189 n
= snprintf ( buf
, BUFSIZ
, "PASS %s \r\n " , pass
);
193 n
= snprintf ( buf
, BUFSIZ
, "NICK %s \r\n " , nick
);
195 n
= snprintf ( buf
, BUFSIZ
, "USER %s 0 * :%s \r\n " , nick
, nick
);
198 if ( pass_type
== SASL_PLAIN_PASSWD
) {
200 /* should wait for 'CAP <nick|*> ACK :<...>' */
201 WRITE ( sock
, "AUTHENTICATE PLAIN \r\n " , 20 );
202 /* server sends 'AUTHENTICATE +' */
203 /* split base64-output into 400 byte chunks; if last is exactly
204 400 bytes, send empty msg ('+') afterwards */
205 n
= snprintf ( buf
, BUFSIZ
, "AUTHENTICATE " );
206 n2
= snprintf ( buf
+ n
, BUFSIZ
- n
, "%s%c%s%c%s" , nick
, 0 , nick
, 0 , pass
);
207 irc_base64 ( buf
+ n
, n2
) OR_DIE
;
208 snprintf ( buf
+ n
+ n2
, BUFSIZ
- n
- n2
, " \r\n " );
209 WRITE ( sock
, buf
, n
+ n2
+ 2 );
210 /* wait for response 900+903 (ok) or 904 (err) */
211 WRITE ( sock
, "CAP END \r\n " , 9 );
214 /* block until we get a RPL_WELCOME or an error: */
216 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
217 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
218 write ( outfd
, buf
, n
);
219 n
= irc_answer ( sock
, buf
, NICK
);
221 else if ( n
& ERRS
) return - 1 ; /* mostly for 433, 464 */
226 n
= snprintf ( buf
, BUFSIZ
, "JOIN %s \r\n " , chan
);
229 /* block until we get a JOIN response or an error: */
230 /* todo: dedup this block with NICK/RPL_WELCOME */
232 if ( poll ( fds
, 1 , POLL_TIMEOUT
)) {
233 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
234 write ( outfd
, buf
, n
);
235 n
= irc_answer ( sock
, buf
, JOIN
);
237 else if ( n
& ERRS
) return - 1 ; /* mostly for 403, 471-475 */
245 int irc_poll ( const sock_t sock
, const int infd
, const int outfd
) {
249 struct pollfd fds
[ 2 ];
250 fds
[ IRC
]. fd
= sock
. fd
;
251 fds
[ IRC
]. events
= POLLIN
;
253 fds
[ CLI
]. events
= POLLIN
;
256 poll ( fds
, 2 , POLL_TIMEOUT
) OR_DIE
;
258 /* XXX: long responses don't get fully processed until user input */
259 /* XXX: must handle TLS_WANT_POLLIN and TLS_WANT_POLLOUT for READ and WRITE! */
260 if ( fds
[ IRC
]. revents
& POLLIN
) {
261 n
= READ ( sock
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
262 if ( n
== 0 ) return - 1 ; /* server closed connection */
263 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
264 write ( outfd
, buf
, n
);
265 irc_answer ( sock
, buf
, NO_CMD
);
266 /* update last-msg-rcvd here */
268 if ( fds
[ CLI
]. revents
& POLLIN
) {
269 n
= read ( infd
, buf
, BUFSIZ
); buf
[ n
] = '\0' ;
270 if ( n
== 0 ) return 0 ; /* we closed connection */
271 n
= WRITE ( sock
, buf
, n
);
272 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
274 if ( fds
[ IRC
]. revents
& POLLOUT
) { /* needed for TLS only */
275 n
= WRITE ( sock
, buf
, n
);
276 fds
[ IRC
]. events
= POLLIN
| ( n
== TLS_WANT_POLLOUT
? POLLOUT
: 0 );
278 /* TODO: if read/write on either irc or cli returns -1 and errno is EAGAIN or EINTR, retry. otherwise, return with error */
282 dprintf(sockfd, "PING\r\n");
284 if (irc_answer(sockfd, buf, NICK) & NICK) break;
289 void irc_cleanup ( const sock_t sock
) {
290 WRITE ( sock
, "QUIT :ircpipe \r\n " , 15 );
291 if ( sock
. tls
) tls_close ( sock
. tls
);
292 shutdown ( sock
. fd
, SHUT_RDWR
);
296 int main ( int argc
, char ** argv
) {
302 size_t ping_iv
= DEFAULT_PING
; /* interval between outgoing pings */
303 size_t resp_to
= DEFAULT_TIMEOUT
; /* how long to wait for command response (connect, ping, auth, ...) */
304 int tls
= DEFAULT_TLS
;
305 char * ca_file
= NULL
;
306 int pass_type
= NO_PASSWD
;
313 pass
= getenv ( "IRC_PASSWD" );
314 ca_file
= getenv ( "IRC_CAFILE" );
316 while (( opt
= getopt ( argc
, argv
, "n:j:pPsSkh" )) != - 1 ) {
318 case 'n' : nick
= optarg
; break ;
319 case 'p' : pass_type
= SERVER_PASSWD
; break ;
320 case 'P' : pass_type
= SASL_PLAIN_PASSWD
; break ;
321 case 's' : tls
= USE_TLS
; break ;
322 case 'S' : tls
= NO_TLS
; break ;
323 case 'k' : tls
= INSECURE_TLS
; break ;
324 case 'j' : chan
= optarg
; break ;
325 default : irc_help ( argv
[ 0 ], opt
!= 'h' );
330 host
= argv
[ optind
++];
332 /* too few positional arguments */
333 fprintf ( stderr
, "missing HOST \n " );
334 irc_help ( argv
[ 0 ], 1 );
337 port
= argv
[ optind
++];
339 port
= ( tls
== NO_TLS
)
344 /* too many positional arguments */
345 fprintf ( stderr
, "too many args \n " );
346 irc_help ( argv
[ 0 ], 1 );
349 if ( pass_type
!= NO_PASSWD
&& pass
== NULL
) {
350 fprintf ( stderr
, "must set IRC_PASSWD envvar to use -p/-P \n " );
354 sock
= irc_connect ( host
, port
, tls
, ca_file
); sock
. fd OR_DIE
;
355 irc_setup ( sock
, 1 , nick
, pass
, pass_type
, chan
) OR_DIE_irc
;
356 rv
= irc_poll ( sock
, 0 , 1 );