isync

mailbox synchronization program
git clone https://git.code.sf.net/p/isync/isync
Log | Files | Refs | README | LICENSE

commit ef413490357b55ed16ab795f05e337c4d6497df4
parent d1ee94f02cb84a43a95b0411368e1effca6bd877
Author: Oswald Buddenhagen <ossi@users.sf.net>
Date:   Sun, 23 Jan 2011 13:43:00 +0100

move socket code to a separate file

this makes the layering more obvious

Diffstat:
Msrc/Makefile.am | 2+-
Msrc/drv_imap.c | 541++++---------------------------------------------------------------------------
Msrc/isync.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/socket.c | 514+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 590 insertions(+), 517 deletions(-)

diff --git a/src/Makefile.am b/src/Makefile.am @@ -5,7 +5,7 @@ SUBDIRS = $(compat_dir) bin_PROGRAMS = mbsync mdconvert -mbsync_SOURCES = main.c sync.c config.c util.c drv_imap.c drv_maildir.c +mbsync_SOURCES = main.c sync.c config.c util.c socket.c drv_imap.c drv_maildir.c mbsync_LDADD = -ldb $(SSL_LIBS) $(SOCK_LIBS) noinst_HEADERS = isync.h diff --git a/src/drv_imap.c b/src/drv_imap.c @@ -22,21 +22,10 @@ * despite that library's more restrictive license. */ -/* This must come before isync.h to avoid our #define S messing up - * blowfish.h on MacOS X. */ -#include <config.h> -#ifdef HAVE_LIBSSL -# include <openssl/ssl.h> -# include <openssl/err.h> -# include <openssl/hmac.h> -#endif - #include "isync.h" #include <assert.h> #include <unistd.h> -#include <sys/mman.h> -#include <sys/time.h> #include <stdlib.h> #include <stdio.h> #include <stddef.h> @@ -44,33 +33,16 @@ #include <errno.h> #include <string.h> #include <ctype.h> -#include <sys/socket.h> -#include <sys/ioctl.h> -#ifdef HAVE_SYS_FILIO_H -# include <sys/filio.h> -#endif -#include <netinet/in.h> -#include <netinet/tcp.h> -#include <arpa/inet.h> -#include <netdb.h> typedef struct imap_server_conf { struct imap_server_conf *next; char *name; - char *tunnel; - char *host; - int port; + server_conf_t sconf; char *user; char *pass; #ifdef HAVE_LIBSSL - char *cert_file; - unsigned use_imaps:1; unsigned require_ssl:1; - unsigned use_sslv2:1; - unsigned use_sslv3:1; - unsigned use_tlsv1:1; unsigned require_cram:1; - X509_STORE *cert_store; #endif } imap_server_conf_t; @@ -94,20 +66,6 @@ typedef struct _list { int len; } list_t; -typedef struct { - int fd; -#ifdef HAVE_LIBSSL - SSL *ssl; -#endif -} Socket_t; - -typedef struct { - Socket_t sock; - int bytes; - int offset; - char buf[1024]; -} buffer_t; - struct imap_cmd; #define max_in_progress 50 /* make this configurable? */ @@ -124,9 +82,6 @@ typedef struct imap_store { /* command queue */ int nexttag, num_in_progress, literal_pending; struct imap_cmd *in_progress, **in_progress_append; -#ifdef HAVE_LIBSSL - SSL_CTX *SSLContext; -#endif /* Used during sequential operations like connect */ enum { GreetingPending = 0, GreetingBad, GreetingOk, GreetingPreauth } greeting; @@ -231,305 +186,6 @@ static const char *Flags[] = { "Deleted", }; -#ifdef HAVE_LIBSSL -/* Some of this code is inspired by / lifted from mutt. */ - -static int -compare_certificates( X509 *cert, X509 *peercert, - unsigned char *peermd, unsigned peermdlen ) -{ - unsigned char md[EVP_MAX_MD_SIZE]; - unsigned mdlen; - - /* Avoid CPU-intensive digest calculation if the certificates are - * not even remotely equal. */ - if (X509_subject_name_cmp( cert, peercert ) || - X509_issuer_name_cmp( cert, peercert )) - return -1; - - if (!X509_digest( cert, EVP_sha1(), md, &mdlen ) || - peermdlen != mdlen || memcmp( peermd, md, mdlen )) - return -1; - - return 0; -} - -#if OPENSSL_VERSION_NUMBER >= 0x00904000L -#define READ_X509_KEY(fp, key) PEM_read_X509( fp, key, 0, 0 ) -#else -#define READ_X509_KEY(fp, key) PEM_read_X509( fp, key, 0 ) -#endif - -/* this gets called when a certificate is to be verified */ -static int -verify_cert( imap_store_t *ctx ) -{ - imap_server_conf_t *srvc = ((imap_store_conf_t *)ctx->gen.conf)->server; - SSL *ssl = ctx->buf.sock.ssl; - X509 *cert, *lcert; - BIO *bio; - FILE *fp; - int err; - unsigned n, i; - X509_STORE_CTX xsc; - char buf[256]; - unsigned char md[EVP_MAX_MD_SIZE]; - - cert = SSL_get_peer_certificate( ssl ); - if (!cert) { - error( "Error, no server certificate\n" ); - return -1; - } - - while (srvc->cert_file) { // So break works - if (X509_cmp_current_time( X509_get_notBefore( cert )) >= 0) { - error( "Server certificate is not yet valid" ); - break; - } - if (X509_cmp_current_time( X509_get_notAfter( cert )) <= 0) { - error( "Server certificate has expired" ); - break; - } - if (!X509_digest( cert, EVP_sha1(), md, &n )) { - error( "*** Unable to calculate digest\n" ); - break; - } - if (!(fp = fopen( srvc->cert_file, "rt" ))) { - error( "Unable to load CertificateFile '%s': %s\n", - srvc->cert_file, strerror( errno ) ); - return 0; - } - err = -1; - for (lcert = 0; READ_X509_KEY( fp, &lcert ); ) - if (!(err = compare_certificates( lcert, cert, md, n ))) - break; - X509_free( lcert ); - fclose( fp ); - if (!err) - return 0; - break; - } - - if (!srvc->cert_store) { - if (!(srvc->cert_store = X509_STORE_new())) { - error( "Error creating certificate store\n" ); - return -1; - } - if (!X509_STORE_set_default_paths( srvc->cert_store )) - warn( "Error while loading default certificate files: %s\n", - ERR_error_string( ERR_get_error(), 0 ) ); - if (!srvc->cert_file) { - info( "Note: CertificateFile not defined\n" ); - } else if (!X509_STORE_load_locations( srvc->cert_store, srvc->cert_file, 0 )) { - error( "Error while loading certificate file '%s': %s\n", - srvc->cert_file, ERR_error_string( ERR_get_error(), 0 ) ); - return -1; - } - } - - X509_STORE_CTX_init( &xsc, srvc->cert_store, cert, 0 ); - err = X509_verify_cert( &xsc ) > 0 ? 0 : X509_STORE_CTX_get_error( &xsc ); - X509_STORE_CTX_cleanup( &xsc ); - if (!err) - return 0; - error( "Error, can't verify certificate: %s (%d)\n", - X509_verify_cert_error_string( err ), err ); - - X509_NAME_oneline( X509_get_subject_name( cert ), buf, sizeof(buf) ); - info( "\nSubject: %s\n", buf ); - X509_NAME_oneline( X509_get_issuer_name( cert ), buf, sizeof(buf) ); - info( "Issuer: %s\n", buf ); - bio = BIO_new( BIO_s_mem() ); - ASN1_TIME_print( bio, X509_get_notBefore( cert ) ); - memset( buf, 0, sizeof(buf) ); - BIO_read( bio, buf, sizeof(buf) - 1 ); - info( "Valid from: %s\n", buf ); - ASN1_TIME_print( bio, X509_get_notAfter( cert ) ); - memset( buf, 0, sizeof(buf) ); - BIO_read( bio, buf, sizeof(buf) - 1 ); - BIO_free( bio ); - info( " to: %s\n", buf ); - if (!X509_digest( cert, EVP_md5(), md, &n )) { - error( "*** Unable to calculate fingerprint\n" ); - } else { - info( "Fingerprint: " ); - for (i = 0; i < n; i += 2) - info( "%02X%02X ", md[i], md[i + 1] ); - info( "\n" ); - } - - fputs( "\nAccept certificate? [y/N]: ", stderr ); - if (fgets( buf, sizeof(buf), stdin ) && (buf[0] == 'y' || buf[0] == 'Y')) - return 0; - return -1; -} - -static int -init_ssl_ctx( imap_store_t *ctx ) -{ - imap_server_conf_t *srvc = ((imap_store_conf_t *)ctx->gen.conf)->server; - const SSL_METHOD *method; - int options = 0; - - if (srvc->use_tlsv1 && !srvc->use_sslv2 && !srvc->use_sslv3) - method = TLSv1_client_method(); - else - method = SSLv23_client_method(); - ctx->SSLContext = SSL_CTX_new( method ); - - if (!srvc->use_sslv2) - options |= SSL_OP_NO_SSLv2; - if (!srvc->use_sslv3) - options |= SSL_OP_NO_SSLv3; - if (!srvc->use_tlsv1) - options |= SSL_OP_NO_TLSv1; - - SSL_CTX_set_options( ctx->SSLContext, options ); - - /* we check the result of the verification after SSL_connect() */ - SSL_CTX_set_verify( ctx->SSLContext, SSL_VERIFY_NONE, 0 ); - return 0; -} -#endif /* HAVE_LIBSSL */ - -static void -socket_perror( const char *func, Socket_t *sock, int ret ) -{ -#ifdef HAVE_LIBSSL - int err; - - if (sock->ssl) { - switch ((err = SSL_get_error( sock->ssl, ret ))) { - case SSL_ERROR_SYSCALL: - case SSL_ERROR_SSL: - if ((err = ERR_get_error()) == 0) { - if (ret == 0) - error( "SSL_%s: got EOF\n", func ); - else - error( "SSL_%s: %s\n", func, strerror(errno) ); - } else - error( "SSL_%s: %s\n", func, ERR_error_string( err, 0 ) ); - return; - default: - error( "SSL_%s: unhandled SSL error %d\n", func, err ); - break; - } - return; - } -#else - (void)sock; -#endif - if (ret < 0) - perror( func ); - else - error( "%s: unexpected EOF\n", func ); -} - -static int -socket_read( Socket_t *sock, char *buf, int len ) -{ - int n; - - assert( sock->fd >= 0 ); - n = -#ifdef HAVE_LIBSSL - sock->ssl ? SSL_read( sock->ssl, buf, len ) : -#endif - read( sock->fd, buf, len ); - if (n <= 0) { - socket_perror( "read", sock, n ); - close( sock->fd ); - sock->fd = -1; - } - return n; -} - -static int -socket_write( Socket_t *sock, char *buf, int len ) -{ - int n; - - assert( sock->fd >= 0 ); - n = -#ifdef HAVE_LIBSSL - sock->ssl ? SSL_write( sock->ssl, buf, len ) : -#endif - write( sock->fd, buf, len ); - if (n != len) { - socket_perror( "write", sock, n ); - close( sock->fd ); - sock->fd = -1; - } - return n; -} - -static int -socket_pending( Socket_t *sock ) -{ - int num = -1; - - if (ioctl( sock->fd, FIONREAD, &num ) < 0) - return -1; - if (num > 0) - return num; -#ifdef HAVE_LIBSSL - if (sock->ssl) - return SSL_pending( sock->ssl ); -#endif - return 0; -} - -/* simple line buffering */ -static int -buffer_gets( buffer_t * b, char **s ) -{ - int n; - int start = b->offset; - - *s = b->buf + start; - - for (;;) { - /* make sure we have enough data to read the \r\n sequence */ - if (b->offset + 1 >= b->bytes) { - if (start) { - /* shift down used bytes */ - *s = b->buf; - - assert( start <= b->bytes ); - n = b->bytes - start; - - if (n) - memmove( b->buf, b->buf + start, n ); - b->offset -= start; - b->bytes = n; - start = 0; - } - - n = socket_read( &b->sock, b->buf + b->bytes, - sizeof(b->buf) - b->bytes ); - - if (n <= 0) - return -1; - - b->bytes += n; - } - - if (b->buf[b->offset] == '\r') { - assert( b->offset + 1 < b->bytes ); - if (b->buf[b->offset + 1] == '\n') { - b->buf[b->offset] = 0; /* terminate the string */ - b->offset += 2; /* next line */ - if (DFlags & VERBOSE) - puts( *s ); - return 0; - } - } - - b->offset++; - } - /* not reached */ -} - static struct imap_cmd * new_imap_cmd( int size ) { @@ -1270,16 +926,9 @@ imap_cancel_store( store_t *gctx ) { imap_store_t *ctx = (imap_store_t *)gctx; + socket_close( &ctx->buf.sock ); free_generic_messages( ctx->gen.msgs ); free_string_list( ctx->gen.boxes ); - if (ctx->buf.sock.fd >= 0) - close( ctx->buf.sock.fd ); -#ifdef HAVE_LIBSSL - if (ctx->buf.sock.ssl) - SSL_free( ctx->buf.sock.ssl ); - if (ctx->SSLContext) - SSL_CTX_free( ctx->SSLContext ); -#endif free_list( ctx->ns_personal ); free_list( ctx->ns_other ); free_list( ctx->ns_shared ); @@ -1370,87 +1019,6 @@ imap_cleanup_p2( imap_store_t *ctx, #ifdef HAVE_LIBSSL static int -start_tls( imap_store_t *ctx ) -{ - int ret; - static int ssl_inited; - - if (!ssl_inited) { - SSL_library_init(); - SSL_load_error_strings(); - ssl_inited = 1; - } - - if (init_ssl_ctx( ctx )) - return 1; - - ctx->buf.sock.ssl = SSL_new( ctx->SSLContext ); - SSL_set_fd( ctx->buf.sock.ssl, ctx->buf.sock.fd ); - if ((ret = SSL_connect( ctx->buf.sock.ssl )) <= 0) { - socket_perror( "connect", &ctx->buf.sock, ret ); - return 1; - } - - /* verify the server certificate */ - if (verify_cert( ctx )) - return 1; - - info( "Connection is now encrypted\n" ); - return 0; -} - -#define ENCODED_SIZE(n) (4*((n+2)/3)) - -static char -hexchar( unsigned int b ) -{ - if (b < 10) - return '0' + b; - return 'a' + (b - 10); -} - -static void -cram( const char *challenge, const char *user, const char *pass, char **_final, int *_finallen ) -{ - unsigned char *response, *final; - unsigned hashlen; - int i, clen, rlen, blen, flen, olen; - unsigned char hash[16]; - char buf[256], hex[33]; - HMAC_CTX hmac; - - HMAC_Init( &hmac, (unsigned char *)pass, strlen( pass ), EVP_md5() ); - - clen = strlen( challenge ); - /* response will always be smaller than challenge because we are decoding. */ - response = nfcalloc( 1 + clen ); - rlen = EVP_DecodeBlock( response, (unsigned char *)challenge, clen ); - HMAC_Update( &hmac, response, rlen ); - free( response ); - - hashlen = sizeof(hash); - HMAC_Final( &hmac, hash, &hashlen ); - assert( hashlen == sizeof(hash) ); - - hex[32] = 0; - for (i = 0; i < 16; i++) { - hex[2 * i] = hexchar( (hash[i] >> 4) & 0xf ); - hex[2 * i + 1] = hexchar( hash[i] & 0xf ); - } - - blen = nfsnprintf( buf, sizeof(buf), "%s %s", user, hex ); - - flen = ENCODED_SIZE( blen ); - final = nfmalloc( flen + 1 ); - final[flen] = 0; - olen = EVP_EncodeBlock( (unsigned char *)final, (unsigned char *)buf, blen ); - assert( olen == flen ); - - *_final = (char *)final; - *_finallen = flen; -} - -static int do_cram_auth( imap_store_t *ctx, struct imap_cmd *cmdp, const char *prompt ) { imap_server_conf_t *srvc = ((imap_store_conf_t *)ctx->gen.conf)->server; @@ -1495,9 +1063,6 @@ imap_open_store( store_conf_t *conf, imap_server_conf_t *srvc = cfg->server; imap_store_t *ctx; store_t **ctxp; - struct hostent *he; - struct sockaddr_in addr; - int s, a[2]; for (ctxp = &unowned; (ctx = (imap_store_t *)*ctxp); ctxp = &ctx->gen.next) if (((imap_store_conf_t *)ctx->gen.conf)->server == srvc) { @@ -1524,68 +1089,12 @@ imap_open_store( store_conf_t *conf, set_bad_callback( &ctx->gen, (void (*)(void *))imap_open_store_bail, ctx ); ctx->in_progress_append = &ctx->in_progress; - /* open connection to IMAP server */ - if (srvc->tunnel) { - infon( "Starting tunnel '%s'... ", srvc->tunnel ); - - if (socketpair( PF_UNIX, SOCK_STREAM, 0, a )) { - perror( "socketpair" ); - exit( 1 ); - } - - if (fork() == 0) { - if (dup2( a[0], 0 ) == -1 || dup2( a[0], 1 ) == -1) - _exit( 127 ); - close( a[0] ); - close( a[1] ); - execl( "/bin/sh", "sh", "-c", srvc->tunnel, (char *)0 ); - _exit( 127 ); - } - - close (a[0]); - - ctx->buf.sock.fd = a[1]; - - info( "ok\n" ); - } else { - memset( &addr, 0, sizeof(addr) ); - addr.sin_port = srvc->port ? htons( srvc->port ) : -#ifdef HAVE_LIBSSL - srvc->use_imaps ? htons( 993 ) : -#endif - htons( 143 ); - addr.sin_family = AF_INET; - - infon( "Resolving %s... ", srvc->host ); - he = gethostbyname( srvc->host ); - if (!he) { - error( "IMAP error: Cannot resolve server '%s'\n", srvc->host ); - goto bail; - } - info( "ok\n" ); - - addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]); - - s = socket( PF_INET, SOCK_STREAM, 0 ); - if (s < 0) { - perror( "socket" ); - exit( 1 ); - } - - infon( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) ); - if (connect( s, (struct sockaddr *)&addr, sizeof(addr) )) { - close( s ); - perror( "connect" ); - goto bail; - } - info( "ok\n" ); - - ctx->buf.sock.fd = s; - } + if (!socket_connect( &srvc->sconf, &ctx->buf.sock )) + goto bail; #ifdef HAVE_LIBSSL - if (srvc->use_imaps) { - if (start_tls( ctx )) { + if (srvc->sconf.use_imaps) { + if (socket_start_tls( &srvc->sconf, &ctx->buf.sock )) { imap_open_store_ssl_bail( ctx ); return; } @@ -1630,7 +1139,8 @@ imap_open_store_authenticate( imap_store_t *ctx ) imap_store_conf_t *cfg = (imap_store_conf_t *)ctx->gen.conf; imap_server_conf_t *srvc = cfg->server; - if (!srvc->use_imaps && (srvc->use_sslv2 || srvc->use_sslv3 || srvc->use_tlsv1)) { + if (!srvc->sconf.use_imaps && + (srvc->sconf.use_sslv2 || srvc->sconf.use_sslv3 || srvc->sconf.use_tlsv1)) { /* always try to select SSL support if available */ if (CAP(STARTTLS)) { imap_exec( ctx, 0, imap_open_store_authenticate_p2, "STARTTLS" ); @@ -1658,7 +1168,7 @@ imap_open_store_authenticate_p2( imap_store_t *ctx, struct imap_cmd *cmd ATTR_UN { if (response != RESP_OK) imap_open_store_bail( ctx ); - else if (start_tls( ctx )) + else if (socket_start_tls( &((imap_server_conf_t *)ctx->gen.conf)->sconf, &ctx->buf.sock )) imap_open_store_ssl_bail( ctx ); else imap_exec( ctx, 0, imap_open_store_authenticate_p3, "CAPABILITY" ); @@ -1797,8 +1307,7 @@ static void imap_open_store_ssl_bail( imap_store_t *ctx ) { /* This avoids that we try to send LOGOUT to an unusable socket. */ - close( ctx->buf.sock.fd ); - ctx->buf.sock.fd = -1; + socket_close( &ctx->buf.sock ); imap_open_store_bail( ctx ); } #endif @@ -2197,7 +1706,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) * case people forget to turn it on */ server->require_ssl = 1; - server->use_tlsv1 = 1; + server->sconf.use_tlsv1 = 1; #endif while (getcline( cfg ) && cfg->cmd) { @@ -2206,9 +1715,9 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) #ifdef HAVE_LIBSSL if (!memcmp( "imaps:", cfg->val, 6 )) { cfg->val += 6; - server->use_imaps = 1; - server->use_sslv2 = 1; - server->use_sslv3 = 1; + server->sconf.use_imaps = 1; + server->sconf.use_sslv2 = 1; + server->sconf.use_sslv3 = 1; } else #endif { @@ -2217,37 +1726,37 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) } if (!memcmp( "//", cfg->val, 2 )) cfg->val += 2; - server->host = nfstrdup( cfg->val ); + server->sconf.host = nfstrdup( cfg->val ); } else if (!strcasecmp( "User", cfg->cmd )) server->user = nfstrdup( cfg->val ); else if (!strcasecmp( "Pass", cfg->cmd )) server->pass = nfstrdup( cfg->val ); else if (!strcasecmp( "Port", cfg->cmd )) - server->port = parse_int( cfg ); + server->sconf.port = parse_int( cfg ); #ifdef HAVE_LIBSSL else if (!strcasecmp( "CertificateFile", cfg->cmd )) { - server->cert_file = expand_strdup( cfg->val ); - if (access( server->cert_file, R_OK )) { + server->sconf.cert_file = expand_strdup( cfg->val ); + if (access( server->sconf.cert_file, R_OK )) { error( "%s:%d: CertificateFile '%s': %s\n", - cfg->file, cfg->line, server->cert_file, strerror( errno ) ); + cfg->file, cfg->line, server->sconf.cert_file, strerror( errno ) ); *err = 1; } } else if (!strcasecmp( "RequireSSL", cfg->cmd )) server->require_ssl = parse_bool( cfg ); else if (!strcasecmp( "UseIMAPS", cfg->cmd )) - server->use_imaps = parse_bool( cfg ); + server->sconf.use_imaps = parse_bool( cfg ); else if (!strcasecmp( "UseSSLv2", cfg->cmd )) - server->use_sslv2 = parse_bool( cfg ); + server->sconf.use_sslv2 = parse_bool( cfg ); else if (!strcasecmp( "UseSSLv3", cfg->cmd )) - server->use_sslv3 = parse_bool( cfg ); + server->sconf.use_sslv3 = parse_bool( cfg ); else if (!strcasecmp( "UseTLSv1", cfg->cmd )) - server->use_tlsv1 = parse_bool( cfg ); + server->sconf.use_tlsv1 = parse_bool( cfg ); else if (!strcasecmp( "RequireCRAM", cfg->cmd )) server->require_cram = parse_bool( cfg ); #endif else if (!strcasecmp( "Tunnel", cfg->cmd )) - server->tunnel = nfstrdup( cfg->val ); + server->sconf.tunnel = nfstrdup( cfg->val ); else if (store) { if (!strcasecmp( "Account", cfg->cmd )) { for (srv = servers; srv; srv = srv->next) @@ -2273,7 +1782,7 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) acc_opt = 1; } if (!store || !store->server) { - if (!server->tunnel && !server->host) { + if (!server->sconf.tunnel && !server->sconf.host) { if (store) error( "IMAP store '%s' has incomplete/missing connection details\n", store->gen.name ); else diff --git a/src/isync.h b/src/isync.h @@ -52,6 +52,41 @@ #define EXE "mbsync" +typedef struct ssl_st SSL; +typedef struct ssl_ctx_st SSL_CTX; +typedef struct x509_store_st X509_STORE; + +typedef struct server_conf { + char *tunnel; + char *host; + int port; +#ifdef HAVE_LIBSSL + char *cert_file; + unsigned use_imaps:1; + unsigned use_sslv2:1; + unsigned use_sslv3:1; + unsigned use_tlsv1:1; + + /* these are actually variables and are leaked at the end */ + SSL_CTX *SSLContext; + X509_STORE *cert_store; +#endif +} server_conf_t; + +typedef struct { + int fd; +#ifdef HAVE_LIBSSL + SSL *ssl; +#endif +} Socket_t; + +typedef struct { + Socket_t sock; + int bytes; + int offset; + char buf[1024]; +} buffer_t; + typedef struct { const char *file; FILE *fp; @@ -295,6 +330,21 @@ extern char Hostname[256]; extern const char *Home; +/* socket.c */ + +int socket_connect( const server_conf_t *conf, Socket_t *sock ); +int socket_start_tls( const server_conf_t *conf, Socket_t *sock ); +void socket_close( Socket_t *sock ); +int socket_read( Socket_t *sock, char *buf, int len ); +int socket_write( Socket_t *sock, char *buf, int len ); +int socket_pending( Socket_t *sock ); + +int buffer_gets( buffer_t *b, char **s ); + +void cram( const char *challenge, const char *user, const char *pass, + char **_final, int *_finallen ); + + /* util.c */ #define DEBUG 1 diff --git a/src/socket.c b/src/socket.c @@ -0,0 +1,514 @@ +/* + * mbsync - mailbox synchronizer + * Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org> + * Copyright (C) 2002-2006,2008,2010,2011 Oswald Buddenhagen <ossi@users.sf.net> + * Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * + * As a special exception, mbsync may be linked with the OpenSSL library, + * despite that library's more restrictive license. + */ + +/* This must come before isync.h to avoid our #define S messing up + * blowfish.h on MacOS X. */ +#include <config.h> +#ifdef HAVE_LIBSSL +# include <openssl/ssl.h> +# include <openssl/err.h> +# include <openssl/hmac.h> +#endif + +#include "isync.h" + +#include <assert.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#ifdef HAVE_SYS_FILIO_H +# include <sys/filio.h> +#endif +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> +#include <netdb.h> + +static void +socket_perror( const char *func, Socket_t *sock, int ret ) +{ +#ifdef HAVE_LIBSSL + int err; + + if (sock->ssl) { + switch ((err = SSL_get_error( sock->ssl, ret ))) { + case SSL_ERROR_SYSCALL: + case SSL_ERROR_SSL: + if ((err = ERR_get_error()) == 0) { + if (ret == 0) + error( "SSL_%s: got EOF\n", func ); + else + error( "SSL_%s: %s\n", func, strerror(errno) ); + } else + error( "SSL_%s: %s\n", func, ERR_error_string( err, 0 ) ); + return; + default: + error( "SSL_%s: unhandled SSL error %d\n", func, err ); + break; + } + return; + } +#else + (void)sock; +#endif + if (ret < 0) + perror( func ); + else + error( "%s: unexpected EOF\n", func ); +} + +#ifdef HAVE_LIBSSL +/* Some of this code is inspired by / lifted from mutt. */ + +static int +compare_certificates( X509 *cert, X509 *peercert, + unsigned char *peermd, unsigned peermdlen ) +{ + unsigned char md[EVP_MAX_MD_SIZE]; + unsigned mdlen; + + /* Avoid CPU-intensive digest calculation if the certificates are + * not even remotely equal. */ + if (X509_subject_name_cmp( cert, peercert ) || + X509_issuer_name_cmp( cert, peercert )) + return -1; + + if (!X509_digest( cert, EVP_sha1(), md, &mdlen ) || + peermdlen != mdlen || memcmp( peermd, md, mdlen )) + return -1; + + return 0; +} + +#if OPENSSL_VERSION_NUMBER >= 0x00904000L +#define READ_X509_KEY(fp, key) PEM_read_X509( fp, key, 0, 0 ) +#else +#define READ_X509_KEY(fp, key) PEM_read_X509( fp, key, 0 ) +#endif + +/* this gets called when a certificate is to be verified */ +static int +verify_cert( const server_conf_t *conf, Socket_t *sock ) +{ + server_conf_t *mconf = (server_conf_t *)conf; + SSL *ssl = sock->ssl; + X509 *cert, *lcert; + BIO *bio; + FILE *fp; + int err; + unsigned n, i; + X509_STORE_CTX xsc; + char buf[256]; + unsigned char md[EVP_MAX_MD_SIZE]; + + cert = SSL_get_peer_certificate( ssl ); + if (!cert) { + error( "Error, no server certificate\n" ); + return -1; + } + + while (conf->cert_file) { /* while() instead of if() so break works */ + if (X509_cmp_current_time( X509_get_notBefore( cert )) >= 0) { + error( "Server certificate is not yet valid" ); + break; + } + if (X509_cmp_current_time( X509_get_notAfter( cert )) <= 0) { + error( "Server certificate has expired" ); + break; + } + if (!X509_digest( cert, EVP_sha1(), md, &n )) { + error( "*** Unable to calculate digest\n" ); + break; + } + if (!(fp = fopen( conf->cert_file, "rt" ))) { + error( "Unable to load CertificateFile '%s': %s\n", + conf->cert_file, strerror( errno ) ); + return 0; + } + err = -1; + for (lcert = 0; READ_X509_KEY( fp, &lcert ); ) + if (!(err = compare_certificates( lcert, cert, md, n ))) + break; + X509_free( lcert ); + fclose( fp ); + if (!err) + return 0; + break; + } + + if (!mconf->cert_store) { + if (!(mconf->cert_store = X509_STORE_new())) { + error( "Error creating certificate store\n" ); + return -1; + } + if (!X509_STORE_set_default_paths( mconf->cert_store )) + warn( "Error while loading default certificate files: %s\n", + ERR_error_string( ERR_get_error(), 0 ) ); + if (!conf->cert_file) { + info( "Note: CertificateFile not defined\n" ); + } else if (!X509_STORE_load_locations( mconf->cert_store, conf->cert_file, 0 )) { + error( "Error while loading certificate file '%s': %s\n", + conf->cert_file, ERR_error_string( ERR_get_error(), 0 ) ); + return -1; + } + } + + X509_STORE_CTX_init( &xsc, mconf->cert_store, cert, 0 ); + err = X509_verify_cert( &xsc ) > 0 ? 0 : X509_STORE_CTX_get_error( &xsc ); + X509_STORE_CTX_cleanup( &xsc ); + if (!err) + return 0; + error( "Error, can't verify certificate: %s (%d)\n", + X509_verify_cert_error_string( err ), err ); + + X509_NAME_oneline( X509_get_subject_name( cert ), buf, sizeof(buf) ); + info( "\nSubject: %s\n", buf ); + X509_NAME_oneline( X509_get_issuer_name( cert ), buf, sizeof(buf) ); + info( "Issuer: %s\n", buf ); + bio = BIO_new( BIO_s_mem() ); + ASN1_TIME_print( bio, X509_get_notBefore( cert ) ); + memset( buf, 0, sizeof(buf) ); + BIO_read( bio, buf, sizeof(buf) - 1 ); + info( "Valid from: %s\n", buf ); + ASN1_TIME_print( bio, X509_get_notAfter( cert ) ); + memset( buf, 0, sizeof(buf) ); + BIO_read( bio, buf, sizeof(buf) - 1 ); + BIO_free( bio ); + info( " to: %s\n", buf ); + if (!X509_digest( cert, EVP_md5(), md, &n )) { + error( "*** Unable to calculate fingerprint\n" ); + } else { + info( "Fingerprint: " ); + for (i = 0; i < n; i += 2) + info( "%02X%02X ", md[i], md[i + 1] ); + info( "\n" ); + } + + fputs( "\nAccept certificate? [y/N]: ", stderr ); + if (fgets( buf, sizeof(buf), stdin ) && (buf[0] == 'y' || buf[0] == 'Y')) + return 0; + return -1; +} + +static int +init_ssl_ctx( const server_conf_t *conf ) +{ + server_conf_t *mconf = (server_conf_t *)conf; + const SSL_METHOD *method; + int options = 0; + + if (conf->use_tlsv1 && !conf->use_sslv2 && !conf->use_sslv3) + method = TLSv1_client_method(); + else + method = SSLv23_client_method(); + mconf->SSLContext = SSL_CTX_new( method ); + + if (!conf->use_sslv2) + options |= SSL_OP_NO_SSLv2; + if (!conf->use_sslv3) + options |= SSL_OP_NO_SSLv3; + if (!conf->use_tlsv1) + options |= SSL_OP_NO_TLSv1; + + SSL_CTX_set_options( mconf->SSLContext, options ); + + /* we check the result of the verification after SSL_connect() */ + SSL_CTX_set_verify( mconf->SSLContext, SSL_VERIFY_NONE, 0 ); + return 0; +} + +int +socket_start_tls( const server_conf_t *conf, Socket_t *sock ) +{ + int ret; + static int ssl_inited; + + if (!ssl_inited) { + SSL_library_init(); + SSL_load_error_strings(); + ssl_inited = 1; + } + + if (!conf->SSLContext && init_ssl_ctx( conf )) + return 1; + + sock->ssl = SSL_new( ((server_conf_t *)conf)->SSLContext ); + SSL_set_fd( sock->ssl, sock->fd ); + if ((ret = SSL_connect( sock->ssl )) <= 0) { + socket_perror( "connect", sock, ret ); + return 1; + } + + /* verify the server certificate */ + if (verify_cert( conf, sock )) + return 1; + + info( "Connection is now encrypted\n" ); + return 0; +} + +#endif /* HAVE_LIBSSL */ + +int +socket_connect( const server_conf_t *conf, Socket_t *sock ) +{ + struct hostent *he; + struct sockaddr_in addr; + int s, a[2]; + + /* open connection to IMAP server */ + if (conf->tunnel) { + infon( "Starting tunnel '%s'... ", conf->tunnel ); + + if (socketpair( PF_UNIX, SOCK_STREAM, 0, a )) { + perror( "socketpair" ); + exit( 1 ); + } + + if (fork() == 0) { + if (dup2( a[0], 0 ) == -1 || dup2( a[0], 1 ) == -1) + _exit( 127 ); + close( a[0] ); + close( a[1] ); + execl( "/bin/sh", "sh", "-c", conf->tunnel, (char *)0 ); + _exit( 127 ); + } + + close( a[0] ); + sock->fd = a[1]; + } else { + memset( &addr, 0, sizeof(addr) ); + addr.sin_port = conf->port ? htons( conf->port ) : +#ifdef HAVE_LIBSSL + conf->use_imaps ? htons( 993 ) : +#endif + htons( 143 ); + addr.sin_family = AF_INET; + + infon( "Resolving %s... ", conf->host ); + he = gethostbyname( conf->host ); + if (!he) { + error( "IMAP error: Cannot resolve server '%s'\n", conf->host ); + return -1; + } + info( "ok\n" ); + + addr.sin_addr.s_addr = *((int *)he->h_addr_list[0]); + + s = socket( PF_INET, SOCK_STREAM, 0 ); + if (s < 0) { + perror( "socket" ); + exit( 1 ); + } + + infon( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) ); + if (connect( s, (struct sockaddr *)&addr, sizeof(addr) )) { + close( s ); + perror( "connect" ); + return -1; + } + + sock->fd = s; + } + info( "ok\n" ); + return 0; +} + +void +socket_close( Socket_t *sock ) +{ + if (sock->fd >= 0) { + close( sock->fd ); + sock->fd = -1; + } +#ifdef HAVE_LIBSSL + if (sock->ssl) { + SSL_free( sock->ssl ); + sock->ssl = 0; + } +#endif +} + +int +socket_read( Socket_t *sock, char *buf, int len ) +{ + int n; + + assert( sock->fd >= 0 ); + n = +#ifdef HAVE_LIBSSL + sock->ssl ? SSL_read( sock->ssl, buf, len ) : +#endif + read( sock->fd, buf, len ); + if (n <= 0) { + socket_perror( "read", sock, n ); + close( sock->fd ); + sock->fd = -1; + } + return n; +} + +int +socket_write( Socket_t *sock, char *buf, int len ) +{ + int n; + + assert( sock->fd >= 0 ); + n = +#ifdef HAVE_LIBSSL + sock->ssl ? SSL_write( sock->ssl, buf, len ) : +#endif + write( sock->fd, buf, len ); + if (n != len) { + socket_perror( "write", sock, n ); + close( sock->fd ); + sock->fd = -1; + } + return n; +} + +int +socket_pending( Socket_t *sock ) +{ + int num = -1; + + if (ioctl( sock->fd, FIONREAD, &num ) < 0) + return -1; + if (num > 0) + return num; +#ifdef HAVE_LIBSSL + if (sock->ssl) + return SSL_pending( sock->ssl ); +#endif + return 0; +} + +/* simple line buffering */ +int +buffer_gets( buffer_t * b, char **s ) +{ + int n; + int start = b->offset; + + *s = b->buf + start; + + for (;;) { + /* make sure we have enough data to read the \r\n sequence */ + if (b->offset + 1 >= b->bytes) { + if (start) { + /* shift down used bytes */ + *s = b->buf; + + assert( start <= b->bytes ); + n = b->bytes - start; + + if (n) + memmove( b->buf, b->buf + start, n ); + b->offset -= start; + b->bytes = n; + start = 0; + } + + n = socket_read( &b->sock, b->buf + b->bytes, + sizeof(b->buf) - b->bytes ); + + if (n <= 0) + return -1; + + b->bytes += n; + } + + if (b->buf[b->offset] == '\r') { + assert( b->offset + 1 < b->bytes ); + if (b->buf[b->offset + 1] == '\n') { + b->buf[b->offset] = 0; /* terminate the string */ + b->offset += 2; /* next line */ + if (DFlags & VERBOSE) + puts( *s ); + return 0; + } + } + + b->offset++; + } + /* not reached */ +} + +#ifdef HAVE_LIBSSL +/* this isn't strictly socket code, but let's have all OpenSSL use in one file. */ + +#define ENCODED_SIZE(n) (4*((n+2)/3)) + +static char +hexchar( unsigned int b ) +{ + if (b < 10) + return '0' + b; + return 'a' + (b - 10); +} + +void +cram( const char *challenge, const char *user, const char *pass, char **_final, int *_finallen ) +{ + unsigned char *response, *final; + unsigned hashlen; + int i, clen, rlen, blen, flen, olen; + unsigned char hash[16]; + char buf[256], hex[33]; + HMAC_CTX hmac; + + HMAC_Init( &hmac, (unsigned char *)pass, strlen( pass ), EVP_md5() ); + + clen = strlen( challenge ); + /* response will always be smaller than challenge because we are decoding. */ + response = nfcalloc( 1 + clen ); + rlen = EVP_DecodeBlock( response, (unsigned char *)challenge, clen ); + HMAC_Update( &hmac, response, rlen ); + free( response ); + + hashlen = sizeof(hash); + HMAC_Final( &hmac, hash, &hashlen ); + assert( hashlen == sizeof(hash) ); + + hex[32] = 0; + for (i = 0; i < 16; i++) { + hex[2 * i] = hexchar( (hash[i] >> 4) & 0xf ); + hex[2 * i + 1] = hexchar( hash[i] & 0xf ); + } + + blen = nfsnprintf( buf, sizeof(buf), "%s %s", user, hex ); + + flen = ENCODED_SIZE( blen ); + final = nfmalloc( flen + 1 ); + final[flen] = 0; + olen = EVP_EncodeBlock( (unsigned char *)final, (unsigned char *)buf, blen ); + assert( olen == flen ); + + *_final = (char *)final; + *_finallen = flen; +} +#endif