/* backend.c -- IMAP server proxy for Cyrus Murder
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* $Id: backend.c,v 1.63 2010/08/04 18:57:36 wescraig Exp $
*/
#include <config.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <fcntl.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <syslog.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <sasl/sasl.h>
#include <sasl/saslutil.h>
#include "assert.h"
#include "prot.h"
#include "backend.h"
#include "global.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"
#include "iptostring.h"
#include "util.h"
enum {
AUTO_CAPA_BANNER = -1,
AUTO_CAPA_NO = 0,
};
static char *parse_capability(const char str[],
struct protocol_t *prot, unsigned long *capa)
{
char *ret = NULL, *tmp;
struct capa_t *c;
/* look for capabilities in the string */
for (c = prot->capa_cmd.capa; c->str; c++) {
if ((tmp = strstr(str, c->str)) != NULL) {
*capa = *capa | c->flag;
if (c->flag == CAPA_AUTH) {
if (prot->capa_cmd.parse_mechlist)
ret = prot->capa_cmd.parse_mechlist(str, prot);
else
ret = xstrdup(tmp+strlen(c->str));
}
}
}
return ret;
}
static char *ask_capability(struct protstream *pout, struct protstream *pin,
struct protocol_t *prot, unsigned long *capa,
char *banner, int automatic)
{
char str[4096];
char *mechlist = NULL, *ret;
const char *resp;
resp = (automatic == AUTO_CAPA_BANNER) ?
prot->banner.resp : prot->capa_cmd.resp;
if (!automatic) {
/* no capability command */
if (!prot->capa_cmd.cmd) return NULL;
/* request capabilities of server */
prot_printf(pout, "%s", prot->capa_cmd.cmd);
if (prot->capa_cmd.arg) prot_printf(pout, " %s", prot->capa_cmd.arg);
prot_printf(pout, "\r\n");
prot_flush(pout);
}
*capa = 0;
do {
if (prot_fgets(str, sizeof(str), pin) == NULL) break;
if ((ret = parse_capability(str, prot, capa))) {
if (mechlist) free(mechlist);
mechlist = ret;
}
if (!resp) {
/* multiline response with no distinct end (IMAP banner) */
prot_NONBLOCK(pin);
}
if (banner) strncpy(banner, str, 2048);
/* look for the end of the capabilities */
} while (!resp || strncasecmp(str, resp, strlen(resp)));
prot_BLOCK(pin);
return mechlist;
}
static int do_compress(struct backend *s, struct simple_cmd_t *compress_cmd)
{
#ifndef HAVE_ZLIB
return -1;
#else
char buf[1024];
/* send compress command */
prot_printf(s->out, "%s\r\n", compress_cmd->cmd);
prot_flush(s->out);
/* check response */
if (!prot_fgets(buf, sizeof(buf), s->in) ||
strncmp(buf, compress_cmd->ok, strlen(compress_cmd->ok)))
return -1;
prot_setcompress(s->in);
prot_setcompress(s->out);
return 0;
#endif /* HAVE_ZLIB */
}
static int do_starttls(struct backend *s, struct tls_cmd_t *tls_cmd)
{
#ifndef HAVE_SSL
return -1;
#else
char buf[2048];
int r;
int *layerp;
char *auth_id;
sasl_ssf_t ssf;
/* send starttls command */
prot_printf(s->out, "%s\r\n", tls_cmd->cmd);
prot_flush(s->out);
/* check response */
if (!prot_fgets(buf, sizeof(buf), s->in) ||
strncmp(buf, tls_cmd->ok, strlen(tls_cmd->ok)))
return -1;
r = tls_init_clientengine(5, "", "");
if (r == -1) return -1;
/* SASL and openssl have different ideas about whether ssf is signed */
layerp = (int *) &ssf;
r = tls_start_clienttls(s->in->fd, s->out->fd, layerp, &auth_id,
&s->tlsconn, &s->tlssess);
if (r == -1) return -1;
r = sasl_setprop(s->saslconn, SASL_SSF_EXTERNAL, &ssf);
if (r == SASL_OK)
r = sasl_setprop(s->saslconn, SASL_AUTH_EXTERNAL, auth_id);
if (auth_id) free(auth_id);
if (r != SASL_OK) return -1;
prot_settls(s->in, s->tlsconn);
prot_settls(s->out, s->tlsconn);
return 0;
#endif /* HAVE_SSL */
}
static char *intersect_mechlists( char *config, char *server )
{
char *newmechlist = xzmalloc( strlen( config ) + 1 );
char *cmech = NULL, *smech = NULL, *s;
int count = 0;
char csave, ssave;
do {
if ( isalnum( *config ) || *config == '_' || *config == '-' ) {
if ( cmech == NULL ) {
cmech = config;
}
} else {
if ( cmech != NULL ) {
csave = *config;
*config = '\0';
s = server;
do {
if ( isalnum( *s ) || *s == '_' || *s == '-' ) {
if ( smech == NULL ) {
smech = s;
}
} else {
if ( smech != NULL ) {
ssave = *s;
*s = '\0';
if ( strcasecmp( cmech, smech ) == 0 ) {
if ( count > 0 ) {
strcat( newmechlist, " " );
}
strcat( newmechlist, cmech );
count++;
*s = ssave;
smech = NULL;
break;
}
*s = ssave;
smech = NULL;
}
}
} while ( *s++ );
*config = csave;
cmech = NULL;
}
}
} while ( *config++ );
if ( count == 0 ) {
free( newmechlist );
return( NULL );
}
return( newmechlist );
}
static int backend_authenticate(struct backend *s, struct protocol_t *prot,
char **mechlist, const char *userid,
sasl_callback_t *cb, const char **status)
{
int r;
sasl_security_properties_t secprops =
{ 0, 0xFF, PROT_BUFSIZE, 0, NULL, NULL }; /* default secprops */
struct sockaddr_storage saddr_l, saddr_r;
char remoteip[60], localip[60];
socklen_t addrsize;
int local_cb = 0;
char buf[2048], optstr[128], *p;
const char *mech_conf, *pass;
/* set the IP addresses */
addrsize=sizeof(struct sockaddr_storage);
if (getpeername(s->sock, (struct sockaddr *)&saddr_r, &addrsize) != 0)
return SASL_FAIL;
if(iptostring((struct sockaddr *)&saddr_r, addrsize, remoteip, 60) != 0)
return SASL_FAIL;
addrsize=sizeof(struct sockaddr_storage);
if (getsockname(s->sock, (struct sockaddr *)&saddr_l, &addrsize)!=0)
return SASL_FAIL;
if(iptostring((struct sockaddr *)&saddr_l, addrsize, localip, 60) != 0)
return SASL_FAIL;
if (!cb) {
local_cb = 1;
strlcpy(optstr, s->hostname, sizeof(optstr));
p = strchr(optstr, '.');
if (p) *p = '\0';
strlcat(optstr, "_password", sizeof(optstr));
pass = config_getoverflowstring(optstr, NULL);
if(!pass) pass = config_getstring(IMAPOPT_PROXY_PASSWORD);
cb = mysasl_callbacks(userid,
config_getstring(IMAPOPT_PROXY_AUTHNAME),
config_getstring(IMAPOPT_PROXY_REALM),
pass);
}
/* Require proxying if we have an "interesting" userid (authzid) */
r = sasl_client_new(prot->sasl_service, s->hostname, localip, remoteip, cb,
(userid && *userid ? SASL_NEED_PROXY : 0) |
(prot->sasl_cmd.parse_success ? SASL_SUCCESS_DATA : 0),
&s->saslconn);
if (r != SASL_OK) {
if (local_cb) free_callbacks(cb);
return r;
}
r = sasl_setprop(s->saslconn, SASL_SEC_PROPS, &secprops);
if (r != SASL_OK) {
if (local_cb) free_callbacks(cb);
return r;
}
/* Get SASL mechanism list. We can force a particular
mechanism using a <shorthost>_mechs option */
strcpy(buf, s->hostname);
p = strchr(buf, '.');
if (p) *p = '\0';
strcat(buf, "_mechs");
mech_conf = config_getoverflowstring(buf, NULL);
if(!mech_conf) {
mech_conf = config_getstring(IMAPOPT_FORCE_SASL_CLIENT_MECH);
}
do {
/* If we have a mech_conf, use it */
if (mech_conf && *mechlist) {
char *conf = xstrdup(mech_conf);
char *newmechlist = intersect_mechlists( conf, *mechlist );
if ( newmechlist == NULL ) {
syslog( LOG_INFO, "%s did not offer %s", s->hostname,
mech_conf );
}
free(conf);
free(*mechlist);
*mechlist = newmechlist;
}
if (*mechlist) {
/* we now do the actual SASL exchange */
saslclient(s->saslconn, &prot->sasl_cmd, *mechlist,
s->in, s->out, &r, status);
/* garbage collect */
free(*mechlist);
*mechlist = NULL;
}
else r = SASL_NOMECH;
/* If we don't have a usable mech, do TLS and try again */
} while (r == SASL_NOMECH && CAPA(s, CAPA_STARTTLS) &&
do_starttls(s, &prot->tls_cmd) != -1 &&
(*mechlist = ask_capability(s->out, s->in, prot,
&s->capability, NULL,
prot->tls_cmd.auto_capa)));
/* xxx unclear that this is correct */
if (local_cb) free_callbacks(cb);
if (r == SASL_OK) {
prot_setsasl(s->in, s->saslconn);
prot_setsasl(s->out, s->saslconn);
}
/* r == SASL_OK on success */
return r;
}
static volatile sig_atomic_t timedout = 0;
static void timed_out(int sig)
{
if (sig == SIGALRM) {
timedout = 1;
} else {
fatal("Bad signal in timed_out", EC_SOFTWARE);
}
}
struct backend *backend_connect(struct backend *ret_backend, const char *server,
struct protocol_t *prot, const char *userid,
sasl_callback_t *cb, const char **auth_status)
{
/* need to (re)establish connection to server or create one */
int sock = -1;
int r;
int err = -1;
int ask = 1; /* should we explicitly ask for capabilities? */
struct addrinfo hints, *res0 = NULL, *res;
struct sockaddr_un sunsock;
char buf[2048], *mechlist = NULL;
struct sigaction action;
struct backend *ret;
if (!ret_backend) {
ret = xzmalloc(sizeof(struct backend));
strlcpy(ret->hostname, server, sizeof(ret->hostname));
ret->timeout = NULL;
}
else
ret = ret_backend;
if (server[0] == '/') { /* unix socket */
res0 = &hints;
memset(res0, 0, sizeof(struct addrinfo));
res0->ai_family = PF_UNIX;
res0->ai_socktype = SOCK_STREAM;
res0->ai_addr = (struct sockaddr *) &sunsock;
res0->ai_addrlen = sizeof(sunsock.sun_family) + strlen(server) + 1;
#ifdef SIN6_LEN
res0->ai_addrlen += sizeof(sunsock.sun_len);
sunsock.sun_len = res0->ai_addrlen;
#endif
sunsock.sun_family = AF_UNIX;
strlcpy(sunsock.sun_path, server, sizeof(sunsock.sun_path));
/* XXX set that we are preauthed */
/* change hostname to 'config_servername' */
strlcpy(ret->hostname, config_servername, sizeof(ret->hostname));
}
else { /* inet socket */
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
err = getaddrinfo(server, prot->service, &hints, &res0);
if (err) {
syslog(LOG_ERR, "getaddrinfo(%s) failed: %s",
server, gai_strerror(err));
if (!ret_backend) free(ret);
return NULL;
}
}
/* Setup timeout */
timedout = 0;
action.sa_flags = 0;
action.sa_handler = timed_out;
sigemptyset(&action.sa_mask);
if(sigaction(SIGALRM, &action, NULL) < 0)
{
syslog(LOG_ERR, "Setting timeout in backend_connect failed: sigaction: %m");
/* continue anyway */
}
for (res = res0; res; res = res->ai_next) {
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sock < 0)
continue;
alarm(config_getint(IMAPOPT_CLIENT_TIMEOUT));
if (connect(sock, res->ai_addr, res->ai_addrlen) >= 0)
break;
if(errno == EINTR && timedout == 1)
errno = ETIMEDOUT;
close(sock);
sock = -1;
}
/* Remove timeout code */
alarm(0);
signal(SIGALRM, SIG_IGN);
if (sock < 0) {
if (res0 != &hints)
freeaddrinfo(res0);
syslog(LOG_ERR, "connect(%s) failed: %m", server);
if (!ret_backend) free(ret);
return NULL;
}
memcpy(&ret->addr, res->ai_addr, res->ai_addrlen);
if (res0 != &hints)
freeaddrinfo(res0);
ret->in = prot_new(sock, 0);
ret->out = prot_new(sock, 1);
ret->sock = sock;
prot_setflushonread(ret->in, ret->out);
ret->prot = prot;
/* use literal+ to send literals */
prot_setisclient(ret->in, 1);
prot_setisclient(ret->out, 1);
if (prot->banner.auto_capa) {
/* try to get the capabilities from the banner */
mechlist = ask_capability(ret->out, ret->in, prot,
&ret->capability, ret->banner,
AUTO_CAPA_BANNER);
if (mechlist || ret->capability) {
/* found capabilities in banner -> don't ask */
ask = 0;
}
}
else {
do { /* read the initial greeting */
if (!prot_fgets(buf, sizeof(buf), ret->in)) {
syslog(LOG_ERR,
"backend_connect(): couldn't read initial greeting: %s",
ret->in->error ? ret->in->error : "(null)");
if (!ret_backend) free(ret);
close(sock);
return NULL;
}
} while (strncasecmp(buf, prot->banner.resp,
strlen(prot->banner.resp)));
strncpy(ret->banner, buf, 2048);
}
if (ask) {
/* get the capabilities */
mechlist = ask_capability(ret->out, ret->in, prot,
&ret->capability, NULL, AUTO_CAPA_NO);
}
/* now need to authenticate to backend server,
unless we're doing LMTP/CSYNC on a UNIX socket (deliver/sync_client) */
if ((server[0] != '/') ||
(strcmp(prot->sasl_service, "lmtp") &&
strcmp(prot->sasl_service, "csync"))) {
char *mlist = NULL;
const char *my_status;
if ( mechlist ) {
mlist = xstrdup(mechlist); /* backend_auth is destructive */
}
if ((r = backend_authenticate(ret, prot, &mlist, userid,
cb, &my_status))) {
syslog(LOG_ERR, "couldn't authenticate to backend server: %s",
sasl_errstring(r, NULL, NULL));
if (!ret_backend) free(ret);
close(sock);
ret = NULL;
}
else {
const void *ssf;
sasl_getprop(ret->saslconn, SASL_SSF, &ssf);
if (*((sasl_ssf_t *) ssf)) {
/* if we have a SASL security layer, compare SASL mech lists
before/after AUTH to check for a MITM attack */
char *new_mechlist;
int auto_capa = (prot->sasl_cmd.auto_capa == AUTO_CAPA_AUTH_SSF);
if (!strcmp(prot->service, "sieve")) {
/* XXX Hack to handle ManageSieve servers.
* No way to tell from protocol if server will
* automatically send capabilities, so we treat it
* as optional.
*/
char ch;
/* wait and probe for possible auto-capability response */
usleep(250000);
prot_NONBLOCK(ret->in);
if ((ch = prot_getc(ret->in)) != EOF) {
prot_ungetc(ch, ret->in);
} else {
auto_capa = AUTO_CAPA_AUTH_NO;
}
prot_BLOCK(ret->in);
}
/*
* A flawed check: backend_authenticate() may be given a
* NULL mechlist, negotiate SSL, and get a new mechlist.
* This new, correct mechlist won't be visible here.
*/
new_mechlist = ask_capability(ret->out, ret->in, prot,
&ret->capability, NULL, auto_capa);
if (new_mechlist && strcmp(new_mechlist, mechlist)) {
syslog(LOG_ERR, "possible MITM attack:"
"list of available SASL mechanisms changed");
if (!ret_backend) free(ret);
close(sock);
ret = NULL;
}
if (new_mechlist) free(new_mechlist);
}
else if (prot->sasl_cmd.auto_capa == AUTO_CAPA_AUTH_OK) {
/* try to get the capabilities from the AUTH success response */
ret->capability = 0;
if (mechlist) free(mechlist);
mechlist = parse_capability(my_status, prot,
&ret->capability);
}
}
if (mlist) free(mlist);
if (auth_status) *auth_status = my_status;
}
if (mechlist) free(mechlist);
/* start compression if requested and both client/server support it */
if (config_getswitch(IMAPOPT_PROXY_COMPRESS) && ret &&
CAPA(ret, CAPA_COMPRESS) &&
prot->compress_cmd.cmd &&
do_compress(ret, &prot->compress_cmd)) {
syslog(LOG_ERR, "couldn't enable compression on backend server");
if (!ret_backend) free(ret);
close(sock);
ret = NULL;
}
if (!ret_backend) ret_backend = ret;
return ret;
}
int backend_ping(struct backend *s)
{
char buf[1024];
if (!s || !s->prot->ping_cmd.cmd) return 0;
if (s->sock == -1) return -1; /* Disconnected Socket */
prot_printf(s->out, "%s\r\n", s->prot->ping_cmd.cmd);
prot_flush(s->out);
for (;;) {
if (!prot_fgets(buf, sizeof(buf), s->in)) {
/* connection closed? */
return -1;
} else if (s->prot->ping_cmd.unsol &&
!strncmp(s->prot->ping_cmd.unsol, buf,
strlen(s->prot->ping_cmd.unsol))) {
/* unsolicited response */
continue;
} else {
/* success/fail response */
return strncmp(s->prot->ping_cmd.ok, buf,
strlen(s->prot->ping_cmd.ok));
}
}
}
void backend_disconnect(struct backend *s)
{
char buf[1024];
if (!s || s->sock == -1) return;
if (!prot_error(s->in)) {
if (s->prot->logout_cmd.cmd) {
prot_printf(s->out, "%s\r\n", s->prot->logout_cmd.cmd);
prot_flush(s->out);
for (;;) {
if (!prot_fgets(buf, sizeof(buf), s->in)) {
/* connection closed? */
break;
} else if (s->prot->logout_cmd.unsol &&
!strncmp(s->prot->logout_cmd.unsol, buf,
strlen(s->prot->logout_cmd.unsol))) {
/* unsolicited response */
continue;
} else {
/* success/fail response -- don't care either way */
break;
}
}
}
}
/* Flush the incoming buffer */
prot_NONBLOCK(s->in);
prot_fill(s->in);
#ifdef HAVE_SSL
/* Free tlsconn */
if (s->tlsconn) {
tls_reset_servertls(&s->tlsconn);
s->tlsconn = NULL;
}
#endif /* HAVE_SSL */
/* close/free socket & prot layer */
cyrus_close_sock(s->sock);
s->sock = -1;
prot_free(s->in);
prot_free(s->out);
s->in = s->out = NULL;
/* Free saslconn */
if(s->saslconn) {
sasl_dispose(&(s->saslconn));
s->saslconn = NULL;
}
/* free last_result buffer */
buf_free(&s->last_result);
}