]> diplodocus.org Git - nmh/blobdiff - sbr/netsec.c
Move m_getfld's MS_* mbox-type macros to the only user.
[nmh] / sbr / netsec.c
index 7cc9e32b82e308c4818a5b6fc53f90cc6398b591..ab05cf8e85e9235c6f874169bc0b488b40a36ec0 100644 (file)
@@ -1,6 +1,4 @@
-
-/*
- * netsec.c -- Network security routines for handling protocols that
+/* netsec.c -- Network security routines for handling protocols that
  *            require SASL and/or TLS.
  *
  * This code is Copyright (c) 2016, by the authors of nmh.  See the
@@ -11,7 +9,9 @@
 #include <h/mh.h>
 #include <h/utils.h>
 #include <h/netsec.h>
+#include <h/oauth.h>
 #include <stdarg.h>
+#include <sys/select.h>
 
 #ifdef CYRUS_SASL
 #include <sasl/sasl.h>
@@ -30,6 +30,8 @@ static int netsec_get_password(sasl_conn_t *conn, void *context, int id,
                               sasl_secret_t **psecret);
 
 static int sasl_initialized = 0;
+
+#define SASL_MAXRECVBUF 65536
 #endif /* CYRUS_SASL */
 
 #ifdef TLS_SUPPORT
@@ -41,16 +43,25 @@ static SSL_CTX *sslctx = NULL;              /* SSL Context */
 
 #endif /* TLS_SUPPORT */
 
+/* I'm going to hardcode this for now; maybe make it adjustable later? */
+#define NETSEC_BUFSIZE 65536
+
 /*
  * Our context structure, which holds all of the relevant information
  * about a connection.
  */
 
 struct _netsec_context {
-    int ns_fd;                 /* Descriptor for network connection */
+    int ns_readfd;             /* Read descriptor for network connection */
+    int ns_writefd;            /* Write descriptor for network connection */
+    int ns_noclose;            /* Do not close file descriptors if set */
     int ns_snoop;              /* If true, display network data */
-    char *ns_hostname;         /* Hostname we've connected to */
+    int ns_snoop_noend;                /* If true, didn't get a CR/LF on last line */
+    netsec_snoop_callback *ns_snoop_cb; /* Snoop output callback */
+    void *ns_snoop_context;    /* Context data for snoop function */
+    int ns_timeout;            /* Network read timeout, in seconds */
     char *ns_userid;           /* Userid for authentication */
+    char *ns_hostname;         /* Hostname we've connected to */
     unsigned char *ns_inbuffer;        /* Our read input buffer */
     unsigned char *ns_inptr;   /* Our read buffer input pointer */
     unsigned int ns_inbuflen;  /* Length of data in input buffer */
@@ -59,16 +70,21 @@ struct _netsec_context {
     unsigned char *ns_outptr;  /* Output buffer pointer */
     unsigned int ns_outbuflen; /* Output buffer data length */
     unsigned int ns_outbufsize;        /* Output buffer size */
-#ifdef CYRUS_SASL
     char *sasl_mech;           /* User-requested mechanism */
+    char *sasl_chosen_mech;    /* Mechanism chosen by SASL */
+    netsec_sasl_callback sasl_proto_cb; /* SASL callback we use */
+#ifdef OAUTH_SUPPORT
+    char *oauth_service;       /* OAuth2 service name */
+#endif /* OAUTH_SUPPORT */
+#ifdef CYRUS_SASL
     sasl_conn_t *sasl_conn;    /* SASL connection context */
     sasl_ssf_t sasl_ssf;       /* SASL Security Strength Factor */
-    netsec_sasl_callback sasl_proto_cb; /* SASL callback we use */
     sasl_callback_t *sasl_cbs; /* Callbacks used by SASL */
     nmh_creds_t sasl_creds;    /* Credentials (username/password) */
     sasl_secret_t *sasl_secret;        /* SASL password structure */
-    char *sasl_chosen_mech;    /* Mechanism chosen by SASL */
-    int sasl_enabled;          /* If true, SASL is enabled */
+    int sasl_seclayer;         /* If true, SASL security layer is enabled */
+    char *sasl_tmpbuf;         /* Temporary read buffer for decodes */
+    size_t sasl_maxbufsize;    /* Maximum negotiated SASL buffer size */
 #endif /* CYRUS_SASL */
 #ifdef TLS_SUPPORT
     BIO *ssl_io;               /* BIO used for connection I/O */
@@ -77,25 +93,40 @@ struct _netsec_context {
 };
 
 /*
- * Function to allocate error message strings
+ * Function to read data from the actual network socket
+ */
+
+static int netsec_fillread(netsec_context *ns_context, char **errstr);
+
+/*
+ * Code to check the ASCII content of a byte array.
  */
 
-static char *netsec_errstr(const char *format, ...);
+static int checkascii(const unsigned char *byte, size_t len);
 
 /*
  * How this code works, in general.
  *
- * _If_ we are using no encryption or SASL encryption, then we buffer the
- * network data through ns_inbuffer and ns_outbuffer.  That should be
- * relatively self-explanatory.
+ * _If_ we are using no encryption then we buffer the network data
+ * through ns_inbuffer and ns_outbuffer.  That should be relatively
+ * self-explanatory.
+ *
+ * If we use encryption, then ns_inbuffer and ns_outbuffer contain the
+ * cleartext data.  When it comes time to send the encrypted data on the
+ * (either from a flush or the buffer is full) we either use BIO_write()
+ * for TLS or sasl_encode() (followed by a write() for Cyrus-SASL.  For
+ * reads we either use BIO_read() (TLS) or do a network read into a
+ * temporary buffer and use sasl_decode() (Cyrus-SASL).  Note that if
+ * negotiate TLS then we disable SASL encryption.
  *
- * If we are using SSL for encryption, then use a buffering BIO for output
- * (that just easier).  Still do buffering for reads; when we need more
- * data we call the BIO_read() function to fill our local buffer.
+ * We used to use a buffering BIO for the reads/writes for TLS, but it
+ * ended up being complicated to special-case the buffering for everything
+ * except TLS, so the buffering is now unified, no matter which encryption
+ * method is being used (even none).
  *
- * For SASL, we make use of (for now) the Cyrus-SASL library.  For some
- * mechanisms, we implement those mechanisms directly since the Cyrus SASL
- * library doesn't support them (like OAuth).
+ * For SASL authentication, we make use of (for now) the Cyrus-SASL
+ * library.  For some mechanisms, we implement those mechanisms directly
+ * since the Cyrus SASL library doesn't support them (like OAuth).
  */
 
 /*
@@ -105,22 +136,42 @@ static char *netsec_errstr(const char *format, ...);
 netsec_context *
 netsec_init(void)
 {
-    netsec_context *nsc = mh_xmalloc(sizeof(*nsc));
+    netsec_context *nsc;
 
-    nsc->ns_fd = -1;
+    NEW(nsc);
+    nsc->ns_readfd = -1;
+    nsc->ns_writefd = -1;
+    nsc->ns_noclose = 0;
     nsc->ns_snoop = 0;
-    nsc->ns_userid = nsc->ns_hostname = NULL;
-    nsc->ns_inbuffer = nsc->ns_inptr = NULL;
-    nsc->ns_inbuflen = nsc->ns_inbufsize = 0;
-    nsc->ns_outbuffer = nsc->ns_outptr = NULL;
-    nsc->ns_outbuflen = nsc->ns_outbufsize = 0;
+    nsc->ns_snoop_noend = 0;
+    nsc->ns_snoop_cb = NULL;
+    nsc->ns_snoop_context = NULL;
+    nsc->ns_userid = NULL;
+    nsc->ns_hostname = NULL;
+    nsc->ns_timeout = 60;      /* Our default */
+    nsc->ns_inbufsize = NETSEC_BUFSIZE;
+    nsc->ns_inbuffer = mh_xmalloc(nsc->ns_inbufsize);
+    nsc->ns_inptr = nsc->ns_inbuffer;
+    nsc->ns_inbuflen = 0;
+    nsc->ns_outbufsize = NETSEC_BUFSIZE;
+    nsc->ns_outbuffer = mh_xmalloc(nsc->ns_outbufsize);
+    nsc->ns_outptr = nsc->ns_outbuffer;
+    nsc->ns_outbuflen = 0;
+    nsc->sasl_mech = NULL;
+    nsc->sasl_chosen_mech = NULL;
+    nsc->sasl_proto_cb = NULL;
+#ifdef OAUTH_SUPPORT
+    nsc->oauth_service = NULL;
+#endif /* OAUTH_SUPPORT */
 #ifdef CYRUS_SASL
     nsc->sasl_conn = NULL;
-    nsc->sasl_mech = NULL;
     nsc->sasl_cbs = NULL;
     nsc->sasl_creds = NULL;
     nsc->sasl_secret = NULL;
-    nsc->sasl_chosen_mech = NULL;
+    nsc->sasl_ssf = 0;
+    nsc->sasl_seclayer = 0;
+    nsc->sasl_tmpbuf = NULL;
+    nsc->sasl_maxbufsize = 0;
 #endif /* CYRUS_SASL */
 #ifdef TLS_SUPPORT
     nsc->ssl_io = NULL;
@@ -131,41 +182,33 @@ netsec_init(void)
 
 /*
  * Shutdown the connection completely and free all resources.
- * The connection is not closed, however.
  */
 
 void
 netsec_shutdown(netsec_context *nsc)
 {
-    if (nsc->ns_hostname)
-       free(nsc->ns_hostname);
-    if (nsc->ns_userid)
-       free(nsc->ns_userid);
-    if (nsc->ns_inbuffer)
-       free(nsc->ns_inbuffer);
-    if (nsc->ns_outbuffer)
-       free(nsc->ns_outbuffer);
+    mh_xfree(nsc->ns_userid);
+    mh_xfree(nsc->ns_hostname);
+    mh_xfree(nsc->ns_inbuffer);
+    mh_xfree(nsc->ns_outbuffer);
+    mh_xfree(nsc->sasl_mech);
+    mh_xfree(nsc->sasl_chosen_mech);
+#ifdef OAUTH_SERVICE
+    mh_xfree(nsc->oauth_service);
+#endif /* OAUTH_SERVICE */
 #ifdef CYRUS_SASL
     if (nsc->sasl_conn)
        sasl_dispose(&nsc->sasl_conn);
-    if (nsc->sasl_mech)
-       free(nsc->sasl_mech);
-    if (nsc->sasl_cbs)
-       free(nsc->sasl_cbs);
-    if (nsc->sasl_creds) {
-       if (nsc->sasl_creds->password)
-           memset(nsc->sasl_creds->password, 0,
-                  strlen(nsc->sasl_creds->password));
-       free(nsc->sasl_creds);
-    }
+    mh_xfree(nsc->sasl_cbs);
+    if (nsc->sasl_creds)
+       nmh_credentials_free(nsc->sasl_creds);
     if (nsc->sasl_secret) {
        if (nsc->sasl_secret->len > 0) {
            memset(nsc->sasl_secret->data, 0, nsc->sasl_secret->len);
        }
        free(nsc->sasl_secret);
     }
-    if (nsc->sasl_chosen_mech)
-       free(nsc->sasl_chosen_mech);
+    mh_xfree(nsc->sasl_tmpbuf);
 #endif /* CYRUS_SASL */
 #ifdef TLS_SUPPORT
     if (nsc->ssl_io)
@@ -176,6 +219,13 @@ netsec_shutdown(netsec_context *nsc)
        BIO_free_all(nsc->ssl_io);
 #endif /* TLS_SUPPORT */
 
+    if (! nsc->ns_noclose) {
+       if (nsc->ns_readfd != -1)
+           close(nsc->ns_readfd);
+       if (nsc->ns_writefd != -1 && nsc->ns_writefd != nsc->ns_readfd)
+           close(nsc->ns_writefd);
+    }
+
     free(nsc);
 }
 
@@ -184,39 +234,711 @@ netsec_shutdown(netsec_context *nsc)
  */
 
 void
-netsec_set_fd(netsec_context *nsc, int fd)
+netsec_set_fd(netsec_context *nsc, int readfd, int writefd)
+{
+    nsc->ns_readfd = readfd;
+    nsc->ns_writefd = writefd;
+}
+
+/*
+ * Set the userid used for authentication for this context
+ */
+
+void
+netsec_set_userid(netsec_context *nsc, const char *userid)
+{
+    nsc->ns_userid = getcpy(userid);
+}
+
+/*
+ * Set the hostname of the remote host we're connecting to.
+ */
+
+void
+netsec_set_hostname(netsec_context *nsc, const char *hostname)
+{
+    nsc->ns_hostname = mh_xstrdup(hostname);
+}
+
+/*
+ * Get the snoop flag for this connection
+ */
+
+int
+netsec_get_snoop(netsec_context *nsc)
+{
+    return nsc->ns_snoop;
+}
+
+/*
+ * Set the snoop flag for this connection
+ */
+
+void
+netsec_set_snoop(netsec_context *nsc, int snoop)
+{
+    nsc->ns_snoop = snoop;
+}
+
+/*
+ * Set the snoop callback for this connection.
+ */
+
+void netsec_set_snoop_callback(netsec_context *nsc,
+                              netsec_snoop_callback callback, void *context)
+{
+    nsc->ns_snoop_cb = callback;
+    nsc->ns_snoop_context = context;
+}
+
+/*
+ * A base64-decoding snoop callback
+ */
+
+void
+netsec_b64_snoop_decoder(netsec_context *nsc, const char *string, size_t len,
+                        void *context)
+{
+    unsigned char *decoded;
+    size_t decodedlen;
+    int offset;
+    NMH_UNUSED(nsc);
+
+    offset = context ? *((int *) context) : 0;
+
+    if (offset > 0) {
+       /*
+        * Output non-base64 data first.
+        */
+       fprintf(stderr, "%.*s", offset, string);
+       string += offset;
+       len -= offset;
+    }
+
+    if (decodeBase64(string, &decoded, &decodedlen, 1, NULL) == OK) {
+       /*
+        * Some mechanisms produce large binary tokens, which aren't really
+        * readable.  So let's do a simple heuristic.  If the token is greater
+        * than 100 characters _and_ the first 100 bytes are more than 50%
+        * non-ASCII, then don't print the decoded buffer, just the
+        * base64 text.
+        */
+       if (decodedlen > 100 && !checkascii(decoded, 100)) {
+           fprintf(stderr, "%.*s\n", (int) len, string);
+       } else {
+           char *hexified;
+           hexify(decoded, decodedlen, &hexified);
+           fprintf(stderr, "b64<%s>\n", hexified);
+           free(hexified);
+       }
+       free(decoded);
+    } else {
+       fprintf(stderr, "%.*s\n", (int) len, string);
+    }
+}
+
+/*
+ * If the ASCII content is > 50%, return 1
+ */
+
+static int
+checkascii(const unsigned char *bytes, size_t len)
+{
+    size_t count = 0, half = len / 2;
+
+    while (len-- > 0) {
+       if (isascii(*bytes) && isprint(*bytes) && ++count > half)
+           return 1;
+       bytes++;
+       /* No chance by this point */
+       if (count + len < half)
+           return 0;
+    }
+
+    return 0;
+}
+
+/*
+ * Set the read timeout for this connection
+ */
+
+void
+netsec_set_timeout(netsec_context *nsc, int timeout)
+{
+    nsc->ns_timeout = timeout;
+}
+
+/*
+ * Read data from the network.  Basically, return anything in our buffer,
+ * otherwise fill from the network.
+ */
+
+ssize_t
+netsec_read(netsec_context *nsc, void *buffer, size_t size, char **errstr)
+{
+    int retlen;
+
+    /*
+     * If our buffer is empty, then we should fill it now
+     */
+
+    if (nsc->ns_inbuflen == 0) {
+       if (netsec_fillread(nsc, errstr) != OK)
+           return NOTOK;
+    }
+
+    /*
+     * netsec_fillread only returns if the buffer is full, so we can
+     * assume here that this has something in it.
+     */
+
+    retlen = min(size, nsc->ns_inbuflen);
+
+    memcpy(buffer, nsc->ns_inptr, retlen);
+
+    if (retlen == (int) nsc->ns_inbuflen) {
+       /*
+        * We've emptied our buffer, so reset everything.
+        */
+       nsc->ns_inptr = nsc->ns_inbuffer;
+       nsc->ns_inbuflen = 0;
+    } else {
+       nsc->ns_inptr += size;
+       nsc->ns_inbuflen -= size;
+    }
+
+    return OK;
+}
+
+/*
+ * Get a "line" (CR/LF) terminated from the network.
+ *
+ * Okay, we play some games here, so pay attention:
+ *
+ * - Unlike every other function, we return a pointer to the
+ *   existing buffer.  This pointer is valid until you call another
+ *   read function again.
+ * - We NUL-terminate the buffer right at the end, before the CR-LF terminator.
+ * - Technically we look for a LF; if we find a CR right before it, then
+ *   we back up one.
+ * - If your data may contain embedded NULs, this won't work.  You should
+ *   be using netsec_read() in that case.
+ */
+
+char *
+netsec_readline(netsec_context *nsc, size_t *len, char **errstr)
 {
-    nsc->ns_fd = fd;
+    unsigned char *ptr = nsc->ns_inptr;
+    size_t count = 0, offset;
+
+retry:
+    /*
+     * Search through our existing buffer for a LF
+     */
+
+    while (count < nsc->ns_inbuflen) {
+       count++;
+       if (*ptr++ == '\n') {
+           char *sptr = (char *) nsc->ns_inptr;
+           if (count > 1 && *(ptr - 2) == '\r')
+               ptr--;
+           *--ptr = '\0';
+           if (len)
+               *len = ptr - nsc->ns_inptr;
+           nsc->ns_inptr += count;
+           nsc->ns_inbuflen -= count;
+           if (nsc->ns_snoop) {
+#ifdef CYRUS_SASL
+               if (nsc->sasl_seclayer)
+                   fprintf(stderr, "(sasl-decrypted) ");
+#endif /* CYRUS_SASL */
+#ifdef TLS_SUPPORT
+               if (nsc->tls_active)
+                   fprintf(stderr, "(tls-decrypted) ");
+#endif /* TLS_SUPPORT */
+               fprintf(stderr, "<= ");
+               if (nsc->ns_snoop_cb)
+                   nsc->ns_snoop_cb(nsc, sptr, strlen(sptr),
+                                    nsc->ns_snoop_context);
+               else
+                   fprintf(stderr, "%s\n", sptr);
+           }
+           return sptr;
+       }
+    }
+
+    /*
+     * Hm, we didn't find a \n.  If we've already searched half of the input
+     * buffer, return an error.
+     */
+
+    if (count >= nsc->ns_inbufsize / 2) {
+       netsec_err(errstr, "Unable to find a line terminator after %d bytes",
+                  count);
+       return NULL;
+    }
+
+    /*
+     * Okay, get some more network data.  This may move inptr, so regenerate
+     * our ptr value;
+     */
+
+    offset = ptr - nsc->ns_inptr;
+
+    if (netsec_fillread(nsc, errstr) != OK)
+       return NULL;
+
+    ptr = nsc->ns_inptr + offset;
+
+    goto retry;
+
+    return NULL;       /* Should never reach this */
 }
 
 /*
- * Set the remote hostname we've connected to.
+ * Fill our read buffer with some data from the network.
  */
 
-void
-netsec_set_hostname(netsec_context *nsc, const char *hostname)
-{
-    nsc->ns_hostname = getcpy(hostname);
-}
+static int
+netsec_fillread(netsec_context *nsc, char **errstr)
+{
+    unsigned char *end;
+    char *readbuf;
+    size_t readbufsize, remaining, startoffset;
+    int rc;
+
+    /*
+     * If inbuflen is zero, that means the buffer has been emptied
+     * completely.  In that case move inptr back to the start.
+     */
+
+    if (nsc->ns_inbuflen == 0) {
+       nsc->ns_inptr = nsc->ns_inbuffer;
+    }
+
+#if defined(CYRUS_SASL) || defined(TLS_SUPPORT)
+retry:
+#endif /* CYRUS_SASL || TLS_SUPPORT */
+    /*
+     * If we are using TLS and there's anything pending, then skip the
+     * select call
+     */
+#ifdef TLS_SUPPORT
+    if (!nsc->tls_active || BIO_pending(nsc->ssl_io) == 0)
+#endif /* TLS_SUPPORT */
+    {
+       struct timeval tv;
+       fd_set rfds;
+
+       FD_ZERO(&rfds);
+       FD_SET(nsc->ns_readfd, &rfds);
+
+       tv.tv_sec = nsc->ns_timeout;
+       tv.tv_usec = 0;
+
+       rc = select(nsc->ns_readfd + 1, &rfds, NULL, NULL, &tv);
+
+       if (rc == -1) {
+           netsec_err(errstr, "select() while reading failed: %s",
+                      strerror(errno));
+           return NOTOK;
+       }
+
+       if (rc == 0) {
+           netsec_err(errstr, "read() timed out after %d seconds",
+                      nsc->ns_timeout);
+           return NOTOK;
+       }
+
+       /*
+        * At this point, we know that rc is 1, so there's not even any
+        * point to check to see if our descriptor is set in rfds.
+        */
+    }
+
+    /*
+     * Some explanation:
+     *
+     * startoffset is the offset from the beginning of the input
+     * buffer to data that is in our input buffer, but has not yet
+     * been consumed.  This can be non-zero if functions like
+     * netsec_readline() leave leftover data.
+     *
+     * remaining is the remaining amount of unconsumed data in the input
+     * buffer.
+     *
+     * end is a pointer to the end of the valid data + 1; it's where
+     * the next read should go.
+     */
+
+    startoffset = nsc->ns_inptr - nsc->ns_inbuffer;
+    remaining = nsc->ns_inbufsize - (startoffset + nsc->ns_inbuflen);
+    end = nsc->ns_inptr + nsc->ns_inbuflen;
+
+    /*
+     * If we're past the halfway point in our read buffers, shuffle everything
+     * back to the beginning.
+     */
+
+    if (startoffset > nsc->ns_inbufsize / 2) {
+       memmove(nsc->ns_inbuffer, nsc->ns_inptr, nsc->ns_inbuflen);
+       nsc->ns_inptr = nsc->ns_inbuffer;
+       startoffset = 0;
+       remaining = nsc->ns_inbufsize - nsc->ns_inbuflen;
+       end = nsc->ns_inptr + nsc->ns_inbuflen;
+    }
+
+    /*
+     * If we are using TLS, then just read via the BIO.  But we still
+     * use our local buffer.
+     */
+#ifdef TLS_SUPPORT
+    if (nsc->tls_active) {
+       rc = BIO_read(nsc->ssl_io, end, remaining);
+       if (rc == 0) {
+           SSL *ssl;
+           int errcode;
+
+           /*
+            * Check to see if we're supposed to retry; if so,
+            * then go back and read again.
+            */
+
+           if (BIO_should_retry(nsc->ssl_io))
+               goto retry;
+
+           /*
+            * Okay, fine.  Get the real error out of the SSL context.
+            */
+
+           if (BIO_get_ssl(nsc->ssl_io, &ssl) < 1) {
+               netsec_err(errstr, "SSL_read() returned 0, but cannot "
+                          "retrieve SSL context");
+               return NOTOK;
+           }
+
+           errcode = SSL_get_error(ssl, rc);
+           if (errcode == SSL_ERROR_ZERO_RETURN) {
+               netsec_err(errstr, "TLS peer closed remote connection");
+           } else {
+               netsec_err(errstr, "TLS network read failed: %s",
+                          ERR_error_string(ERR_peek_last_error(), NULL));
+           }
+           if (nsc->ns_snoop)
+               ERR_print_errors_fp(stderr);
+           return NOTOK;
+       }
+        if (rc < 0) {
+           /* Definitely an error */
+           netsec_err(errstr, "Read on TLS connection failed: %s",
+                      ERR_error_string(ERR_get_error(), NULL));
+           return NOTOK;
+       }
+
+       nsc->ns_inbuflen += rc;
+
+       return OK;
+    }
+#endif /* TLS_SUPPORT */
+
+    /*
+     * Okay, time to read some data.  Either we're just doing it straight
+     * or we're passing it through sasl_decode() first.
+     */
+
+#ifdef CYRUS_SASL
+    if (nsc->sasl_seclayer) {
+       readbuf = nsc->sasl_tmpbuf;
+       readbufsize = nsc->sasl_maxbufsize;
+    } else
+#endif /* CYRUS_SASL */
+    {
+       readbuf = (char *) end;
+       readbufsize = remaining;
+    }
+
+    /*
+     * At this point, we should have active data on the connection (see
+     * select() above) so this read SHOULDN'T block.  Hopefully.
+     */
+
+    rc = read(nsc->ns_readfd, readbuf, readbufsize);
+
+    if (rc == 0) {
+       netsec_err(errstr, "Received EOF on network read");
+       return NOTOK;
+    }
+
+    if (rc < 0) {
+       netsec_err(errstr, "Network read failed: %s", strerror(errno));
+       return NOTOK;
+    }
+
+    /*
+     * Okay, so we've had a successful read.  If we are doing SASL security
+     * layers, pass this through sasl_decode().  sasl_decode() can return
+     * 0 bytes decoded; if that happens, jump back to the beginning.  Otherwise
+     * we can just update our length pointer.
+     */
+
+#ifdef CYRUS_SASL
+    if (nsc->sasl_seclayer) {
+       const char *tmpout;
+       unsigned int tmpoutlen;
+
+       rc = sasl_decode(nsc->sasl_conn, nsc->sasl_tmpbuf, rc,
+                        &tmpout, &tmpoutlen);
+
+       if (rc != SASL_OK) {
+           netsec_err(errstr, "Unable to decode SASL network data: %s",
+                      sasl_errdetail(nsc->sasl_conn));
+           return NOTOK;
+       }
+
+       if (tmpoutlen == 0)
+           goto retry;
+
+       /*
+        * Just in case ...
+        */
+
+       if (tmpoutlen > remaining) {
+           netsec_err(errstr, "Internal error: SASL decode buffer overflow!");
+           return NOTOK;
+       }
+
+       memcpy(end, tmpout, tmpoutlen);
+
+       nsc->ns_inbuflen += tmpoutlen;
+    } else
+#endif /* CYRUS_SASL */
+       nsc->ns_inbuflen += rc;
+
+    return OK;
+}
+
+/*
+ * Write data to our network connection.  Really, fill up the buffer as
+ * much as we can, and flush it out if necessary.  netsec_flush() does
+ * the real work.
+ */
+
+int
+netsec_write(netsec_context *nsc, const void *buffer, size_t size,
+            char **errstr)
+{
+    const unsigned char *bufptr = buffer;
+    int rc, remaining;
+
+    /* Just in case */
+
+    if (size == 0)
+       return OK;
+
+    /*
+     * Run a loop copying in data to our local buffer; when we're done with
+     * any buffer overflows then just copy any remaining data in.
+     */
+
+    while ((int) size >= (remaining = nsc->ns_outbufsize - nsc->ns_outbuflen)) {
+       memcpy(nsc->ns_outptr, bufptr, remaining);
+
+       /*
+        * In theory I should increment outptr, but netsec_flush just resets
+        * it anyway.
+        */
+       nsc->ns_outbuflen = nsc->ns_outbufsize;
+
+       rc = netsec_flush(nsc, errstr);
+
+       if (rc != OK)
+           return NOTOK;
+
+       bufptr += remaining;
+       size -= remaining;
+    }
+
+    /*
+     * Copy any leftover data into the buffer.
+     */
+
+    if (size > 0) {
+       memcpy(nsc->ns_outptr, bufptr, size);
+       nsc->ns_outptr += size;
+       nsc->ns_outbuflen += size;
+    }
+
+    return OK;
+}
+
+/*
+ * Our network printf() routine, which really just calls netsec_vprintf().
+ */
+
+int
+netsec_printf(netsec_context *nsc, char **errstr, const char *format, ...)
+{
+    va_list ap;
+    int rc;
+
+    va_start(ap, format);
+    rc = netsec_vprintf(nsc, errstr, format, ap);
+    va_end(ap);
+
+    return rc;
+}
+
+/*
+ * Write bytes to the network using printf()-style formatting.
+ *
+ * Again, for the most part copy stuff into our buffer to be flushed
+ * out later.
+ */
+
+int
+netsec_vprintf(netsec_context *nsc, char **errstr, const char *format,
+              va_list ap)
+{
+    int rc;
+
+    /*
+     * Cheat a little.  If we can fit the data into our outgoing buffer,
+     * great!  If not, generate a flush and retry once.
+     */
+
+retry:
+    rc = vsnprintf((char *) nsc->ns_outptr,
+                  nsc->ns_outbufsize - nsc->ns_outbuflen, format, ap);
+
+    if (rc >= (int) (nsc->ns_outbufsize - nsc->ns_outbuflen)) {
+       /*
+        * This means we have an overflow.  Note that we don't actually
+        * make use of the terminating NUL, but according to the spec
+        * vsnprintf() won't write to the last byte in the string; that's
+        * why we have to use >= in the comparison above.
+        */
+       if (nsc->ns_outbuffer == nsc->ns_outptr) {
+           /*
+            * Whoops, if the buffer pointer was the same as the start of the
+            * buffer, that means we overflowed the internal buffer.
+            * At that point, just give up.
+            */
+           netsec_err(errstr, "Internal error: wanted to printf() a total of "
+                      "%d bytes, but our buffer size was only %d bytes",
+                      rc, nsc->ns_outbufsize);
+           return NOTOK;
+       }
+        /*
+         * Generate a flush (which may be inefficient, but hopefully
+         * it isn't) and then try again.
+         */
+        if (netsec_flush(nsc, errstr) != OK)
+            return NOTOK;
+        /*
+         * After this, outbuffer should == outptr, so we shouldn't
+         * hit this next time around.
+         */
+        goto retry;
+    }
+
+    if (nsc->ns_snoop) {
+       int outlen = rc;
+       if (outlen > 0 && nsc->ns_outptr[outlen - 1] == '\n') {
+           outlen--;
+           if (outlen > 0 && nsc->ns_outptr[outlen - 1] == '\r')
+               outlen--;
+       } else {
+           nsc->ns_snoop_noend = 1;
+       }
+       if (outlen > 0 || nsc->ns_snoop_noend == 0) {
+#ifdef CYRUS_SASL
+           if (nsc->sasl_seclayer)
+               fprintf(stderr, "(sasl-encrypted) ");
+#endif /* CYRUS_SASL */
+#ifdef TLS_SUPPORT
+           if (nsc->tls_active)
+               fprintf(stderr, "(tls-encrypted) ");
+#endif /* TLS_SUPPORT */
+           fprintf(stderr, "=> ");
+           if (nsc->ns_snoop_cb)
+               nsc->ns_snoop_cb(nsc, (char *) nsc->ns_outptr, outlen,
+                                nsc->ns_snoop_context);
+           else
+                fprintf(stderr, "%.*s\n", outlen, nsc->ns_outptr); 
+       } else {
+           nsc->ns_snoop_noend = 0;
+       }
+    }
+
+    nsc->ns_outptr += rc;
+    nsc->ns_outbuflen += rc;
+
+    return OK;
+}
+
+/*
+ * Flush out any buffered data in our output buffers.  This routine is
+ * actually where the real network writes take place.
+ */
+
+int
+netsec_flush(netsec_context *nsc, char **errstr)
+{
+    const char *netoutbuf = (const char *) nsc->ns_outbuffer;
+    unsigned int netoutlen = nsc->ns_outbuflen;
+    int rc;
+
+    /*
+     * Small optimization
+     */
+
+    if (netoutlen == 0)
+       return OK;
+
+    /*
+     * If SASL security layers are in effect, run the data through
+     * sasl_encode() first.
+     */
+#ifdef CYRUS_SASL
+    if (nsc->sasl_seclayer) {
+       rc = sasl_encode(nsc->sasl_conn, (const char *) nsc->ns_outbuffer,
+                        nsc->ns_outbuflen, &netoutbuf, &netoutlen);
+
+       if (rc != SASL_OK) {
+           netsec_err(errstr, "SASL data encoding failed: %s",
+                      sasl_errdetail(nsc->sasl_conn));
+           return NOTOK;
+       }
+
+    }
+#endif /* CYRUS_SASL */
 
-/*
- * Set the userid used for authentication for this context
- */
+    /*
+     * If TLS is active, then use those functions to write out the
+     * data.
+     */
+#ifdef TLS_SUPPORT
+    if (nsc->tls_active) {
+       if (BIO_write(nsc->ssl_io, netoutbuf, netoutlen) <= 0) {
+           netsec_err(errstr, "Error writing to TLS connection: %s",
+                      ERR_error_string(ERR_get_error(), NULL));
+           return NOTOK;
+       }
+    } else
+#endif /* TLS_SUPPORT */
+    {
+       rc = write(nsc->ns_writefd, netoutbuf, netoutlen);
 
-void
-netsec_set_userid(netsec_context *nsc, const char *userid)
-{
-    nsc->ns_userid = getcpy(userid);
-}
+       if (rc < 0) {
+           netsec_err(errstr, "write() failed: %s", strerror(errno));
+           return NOTOK;
+       }
+    }
 
-/*
- * Set the snoop flag for this connection
- */
+    nsc->ns_outptr = nsc->ns_outbuffer;
+    nsc->ns_outbuflen = 0;
 
-void
-netsec_set_snoop(netsec_context *nsc, int snoop)
-{
-    nsc->ns_snoop = snoop;
+    return OK;
 }
 
 /*
@@ -224,19 +946,24 @@ netsec_set_snoop(netsec_context *nsc, int snoop)
  */
 
 int
-netsec_set_sasl_params(netsec_context *nsc, const char *hostname,
-                      const char *service, const char *mechanism,
-                      netsec_sasl_callback callback, char **errstr)
+netsec_set_sasl_params(netsec_context *nsc, const char *service,
+                      const char *mechanism, netsec_sasl_callback callback,
+                      char **errstr)
 {
 #ifdef CYRUS_SASL
     sasl_callback_t *sasl_cbs;
     int retval;
 
+    if (!nsc->ns_hostname) {
+       netsec_err(errstr, "Internal error: ns_hostname is NULL");
+       return NOTOK;
+    }
+
     if (! sasl_initialized) {
        retval = sasl_client_init(NULL);
        if (retval != SASL_OK) {
-           *errstr = netsec_errstr("SASL client initialization failed: %s",
-                                   sasl_errstring(retval, NULL, NULL));
+           netsec_err(errstr, "SASL client initialization failed: %s",
+                      sasl_errstring(retval, NULL, NULL));
            return NOTOK;
        }
        sasl_initialized++;
@@ -267,24 +994,44 @@ netsec_set_sasl_params(netsec_context *nsc, const char *hostname,
 
     nsc->sasl_cbs = sasl_cbs;
 
-    retval = sasl_client_new(service, hostname, NULL, NULL, nsc->sasl_cbs, 0,
-                            &nsc->sasl_conn);
+    retval = sasl_client_new(service, nsc->ns_hostname, NULL, NULL,
+                            nsc->sasl_cbs, 0, &nsc->sasl_conn);
 
     if (retval) {
-       *errstr = netsec_errstr("SASL new client allocation failed: %s",
-                               sasl_errstring(retval, NULL, NULL));
+       netsec_err(errstr, "SASL new client allocation failed: %s",
+                  sasl_errstring(retval, NULL, NULL));
        return NOTOK;
     }
 
-    nsc->sasl_mech = mechanism ? getcpy(mechanism) : NULL;
-    nsc->sasl_proto_cb = callback;
+    /*
+     * Set up our credentials
+     */
 
-    return OK;
-#else /* CYRUS_SASL */
-    *errstr = netsec_errstr("SASL is not supported");
+    nsc->sasl_creds = nmh_get_credentials(nsc->ns_hostname, nsc->ns_userid);
 
-    return NOTOK;
+#else /* CYRUS_SASL */
+    NMH_UNUSED(service);
+    NMH_UNUSED(errstr);
 #endif /* CYRUS_SASL */
+
+    /*
+     * According to the RFC, mechanisms can only be uppercase letter, numbers,
+     * and a hyphen or underscore.  So make sure we uppercase any letters
+     * in case the user passed in lowercase.
+     */
+
+    if (mechanism) {
+       char *p;
+       nsc->sasl_mech = mh_xstrdup(mechanism);
+
+       for (p = nsc->sasl_mech; *p; p++)
+           if (isascii((unsigned char) *p)) /* Leave non-ASCII lower alone. */
+               *p = toupper((unsigned char) *p);
+    }
+
+    nsc->sasl_proto_cb = callback;
+
+    return OK;
 }
 
 #ifdef CYRUS_SASL
@@ -301,35 +1048,10 @@ int netsec_get_user(void *context, int id, const char **result,
     if (! result || (id != SASL_CB_USER && id != SASL_CB_AUTHNAME))
        return SASL_BADPARAM;
 
-    if (nsc->ns_userid == NULL) {
-       /*
-        * Pass the 1 third argument to nmh_get_credentials() so that
-        * a defauly user if the -user switch wasn't supplied, and so
-        * that a default password will be supplied.  That's used when
-        * those values really don't matter, and only with legacy/.netrc,
-        * i.e., with a credentials profile entry.
-        */
-
-       if (nsc->sasl_creds == NULL) {
-           nsc->sasl_creds = mh_xmalloc(sizeof(*nsc->sasl_creds));
-           nsc->sasl_creds->user = NULL;
-           nsc->sasl_creds->password = NULL;
-       }
-
-       if (nmh_get_credentials(nsc->ns_hostname, nsc->ns_userid, 1,
-                               nsc->sasl_creds) != OK)
-           return SASL_BADPARAM;
+    *result = nmh_cred_get_user(nsc->sasl_creds);
 
-       if (nsc->ns_userid != nsc->sasl_creds->user) {
-           if (nsc->ns_userid)
-               free(nsc->ns_userid);
-           nsc->ns_userid = getcpy(nsc->sasl_creds->user);
-       }
-    }
-
-    *result = nsc->ns_userid;
     if (len)
-       *len = strlen(nsc->ns_userid);
+       *len = strlen(*result);
 
     return SASL_OK;
 }
@@ -343,6 +1065,7 @@ netsec_get_password(sasl_conn_t *conn, void *context, int id,
                    sasl_secret_t **psecret)
 {
     netsec_context *nsc = (netsec_context *) context;
+    const char *password;
     int len;
 
     NMH_UNUSED(conn);
@@ -350,27 +1073,9 @@ netsec_get_password(sasl_conn_t *conn, void *context, int id,
     if (! psecret || id != SASL_CB_PASS)
        return SASL_BADPARAM;
 
-    if (nsc->sasl_creds == NULL) {
-       nsc->sasl_creds = mh_xmalloc(sizeof(*nsc->sasl_creds));
-       nsc->sasl_creds->user = NULL;
-       nsc->sasl_creds->password = NULL;
-    }
-
-    if (nsc->sasl_creds->password == NULL) {
-       /*
-        * Pass the 0 third argument to nmh_get_credentials() so
-        * that the default password isn't used.  With legacy/.netrc
-        * credentials support, we'll only get here if the -user
-        * switch to send(1)/post(8) wasn't used.
-        */
-
-       if (nmh_get_credentials(nsc->ns_hostname, nsc->ns_userid, 0,
-                               nsc->sasl_creds) != OK) {
-           return SASL_BADPARAM;
-       }
-    }
+    password = nmh_cred_get_password(nsc->sasl_creds);
 
-    len = strlen(nsc->sasl_creds->password);
+    len = strlen(password);
 
     /*
      * sasl_secret_t includes 1 bytes for "data" already, so that leaves
@@ -383,7 +1088,7 @@ netsec_get_password(sasl_conn_t *conn, void *context, int id,
        return SASL_NOMEM;
 
     (*psecret)->len = len;
-    strcpy((char *) (*psecret)->data, nsc->sasl_creds->password);
+    strcpy((char *) (*psecret)->data, password);
 
     nsc->sasl_secret = *psecret;
 
@@ -400,13 +1105,295 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr)
 {
 #ifdef CYRUS_SASL
     sasl_security_properties_t secprops;
-    char *chosen_mech;
+    const char *chosen_mech;
+    const unsigned char *saslbuf;
+    unsigned char *outbuf;
+    unsigned int saslbuflen, outbuflen;
     sasl_ssf_t *ssf;
+    int *outbufmax;
+#endif
+#ifdef OAUTH_SUPPORT
+    unsigned char *xoauth_client_res;
+    size_t xoauth_client_res_len;
+#endif /* OAUTH_SUPPORT */
+#if defined CYRUS_SASL || defined OAUTH_SUPPORT
     int rc;
+#endif /* CYRUS_SASL || OAUTH_SUPPORT */
+
+    /*
+     * If we've been passed a requested mechanism, check our mechanism
+     * list from the protocol.  If it's not supported, return an error.
+     */
+
+    if (nsc->sasl_mech) {
+       char **str, *mlist = getcpy(mechlist);
+       int i;
+
+       str = brkstring(mlist, " ", NULL);
+
+       for (i = 0; str[i] != NULL; i++) {
+           if (strcasecmp(nsc->sasl_mech, str[i]) == 0) {
+               break;
+           }
+       }
+
+       i = (str[i] == NULL);
+
+       free(mlist);
+
+       if (i) {
+           netsec_err(errstr, "Chosen mechanism %s not supported by server",
+                      nsc->sasl_mech);
+           return NOTOK;
+       }
+    }
+
+#ifdef OAUTH_SUPPORT
+    if (nsc->sasl_mech && strcasecmp(nsc->sasl_mech, "XOAUTH2") == 0) {
+       /*
+        * This should be relatively straightforward, but requires some
+        * help from the plugin.  Basically, if XOAUTH2 is a success,
+        * the callback has to return success, but no output data.  If
+        * there is output data, it will be assumed that it is the JSON
+        * error message.
+        */
+
+       if (! nsc->oauth_service) {
+           netsec_err(errstr, "Internal error: OAuth2 service name not given");
+           return NOTOK;
+       }
+
+       nsc->sasl_chosen_mech = mh_xstrdup(nsc->sasl_mech);
+
+       if (mh_oauth_do_xoauth(nsc->ns_userid, nsc->oauth_service,
+                              &xoauth_client_res, &xoauth_client_res_len,
+                              nsc->ns_snoop ? stderr : NULL) != OK) {
+           netsec_err(errstr, "Internal error: Unable to get OAuth2 "
+                      "bearer token");
+           return NOTOK;
+       }
+
+       rc = nsc->sasl_proto_cb(NETSEC_SASL_START, xoauth_client_res,
+                               xoauth_client_res_len, NULL, 0, errstr);
+       free(xoauth_client_res);
+
+       if (rc != OK)
+           return NOTOK;
+
+       /*
+        * Okay, we need to do a NETSEC_SASL_FINISH now.  If we return
+        * success, we indicate that with no output data.  But if we
+        * fail, then send a blank message and get the resulting
+        * error.
+        */
+
+       rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0, errstr);
+
+       if (rc != OK) {
+           /*
+            * We're going to assume the error here is a JSON response;
+            * we ignore it and send a blank message in response.  We should
+            * then get a failure messages with a useful error.  We should
+            * NOT get a success message at this point.
+            */
+           free(*errstr);
+           nsc->sasl_proto_cb(NETSEC_SASL_WRITE, NULL, 0, NULL, 0, NULL);
+           rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+                                   errstr);
+           if (rc == 0) {
+               netsec_err(errstr, "Unexpected success after OAuth failure!");
+           }
+           return NOTOK;
+       }
+       return OK;
+    }
+#endif /* OAUTH_SUPPORT */
+
+#ifdef CYRUS_SASL
+    /*
+     * In netsec_set_sasl_params, we've already done all of our setup with
+     * sasl_client_init() and sasl_client_new().  So time to set security
+     * properties, call sasl_client_start(), and generate the protocol
+     * messages.
+     */
+
+    memset(&secprops, 0, sizeof(secprops));
+    secprops.maxbufsize = SASL_MAXRECVBUF;
+
+    /*
+     * If we're using TLS, do not negotiate a security layer
+     */
+
+    secprops.max_ssf = 
+#ifdef TLS_SUPPORT
+               nsc->tls_active ? 0 :
+#endif /* TLS_SUPPORT */
+               UINT_MAX;
+
+    rc = sasl_setprop(nsc->sasl_conn, SASL_SEC_PROPS, &secprops);
+
+    if (rc != SASL_OK) {
+       netsec_err(errstr, "SASL security property initialization failed: %s",
+                  sasl_errstring(rc, NULL, NULL));
+       return NOTOK;
+    }
+
+    /*
+     * Start the actual protocol negotiation, and go through the
+     * sasl_client_step() loop (after sasl_client_start, of course).
+     */
+
+    rc = sasl_client_start(nsc->sasl_conn,
+                          nsc->sasl_mech ? nsc->sasl_mech : mechlist, NULL,
+                          (const char **) &saslbuf, &saslbuflen,
+                          &chosen_mech);
+
+    if (rc != SASL_OK && rc != SASL_CONTINUE) {
+       netsec_err(errstr, "SASL client start failed: %s",
+                  sasl_errdetail(nsc->sasl_conn));
+       return NOTOK;
+    }
+
+    nsc->sasl_chosen_mech = getcpy(chosen_mech);
+
+    if (nsc->sasl_proto_cb(NETSEC_SASL_START, saslbuf, saslbuflen, NULL, 0,
+                          errstr) != OK)
+       return NOTOK;
+
+    /*
+     * We've written out our first message; enter in the step loop
+     */
+
+    while (rc == SASL_CONTINUE) {
+       /*
+        * Call our SASL callback, which will handle the details of
+        * reading data from the network.
+        */
+
+       if (nsc->sasl_proto_cb(NETSEC_SASL_READ, NULL, 0, &outbuf, &outbuflen,
+                              errstr) != OK) {
+           nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, NULL);
+           return NOTOK;
+       }
+
+       rc = sasl_client_step(nsc->sasl_conn, (char *) outbuf, outbuflen, NULL,
+                             (const char **) &saslbuf, &saslbuflen);
+
+        mh_xfree(outbuf);
+
+       if (rc != SASL_OK && rc != SASL_CONTINUE) {
+           netsec_err(errstr, "SASL client negotiation failed: %s",
+                      sasl_errdetail(nsc->sasl_conn));
+           nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, NULL);
+           return NOTOK;
+       }
+
+       if (nsc->sasl_proto_cb(NETSEC_SASL_WRITE, saslbuf, saslbuflen,
+                              NULL, 0, errstr) != OK) {
+           nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0, NULL);
+           return NOTOK;
+       }
+    }
+
+    /*
+     * SASL exchanges should be complete, process the final response message
+     * from the server.
+     */
+
+    if (nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+                          errstr) != OK) {
+       /*
+        * At this point we can't really send an abort since the SASL dialog
+        * has completed, so just bubble back up the error message.
+        */
+
+       return NOTOK;
+    }
+
+    /*
+     * At this point, SASL should be complete.  Get a few properties
+     * from the authentication exchange.
+     */
+
+    rc = sasl_getprop(nsc->sasl_conn, SASL_SSF, (const void **) &ssf);
+
+    if (rc != SASL_OK) {
+       netsec_err(errstr, "Cannot retrieve SASL negotiated security "
+                 "strength factor: %s", sasl_errstring(rc, NULL, NULL));
+       return NOTOK;
+    }
+
+    nsc->sasl_ssf = *ssf;
+
+    if (nsc->sasl_ssf > 0) {
+       rc = sasl_getprop(nsc->sasl_conn, SASL_MAXOUTBUF,
+                         (const void **) &outbufmax);
+
+       if (rc != SASL_OK) {
+           netsec_err(errstr, "Cannot retrieve SASL negotiated output "
+                      "buffer size: %s", sasl_errstring(rc, NULL, NULL));
+           return NOTOK;
+       }
+
+       /*
+        * If our output buffer isn't the same size as the input buffer,
+        * reallocate it and set the new size (since we won't encode any
+        * data larger than that).
+        */
+
+       nsc->sasl_maxbufsize = *outbufmax;
+
+       if (nsc->ns_outbufsize != nsc->sasl_maxbufsize) {
+           nsc->ns_outbufsize = nsc->sasl_maxbufsize;
+           nsc->ns_outbuffer = mh_xrealloc(nsc->ns_outbuffer,
+                                           nsc->ns_outbufsize);
+           /*
+            * There shouldn't be any data in the buffer, but for
+            * consistency's sake discard it.
+            */
+           nsc->ns_outptr = nsc->ns_outbuffer;
+           nsc->ns_outbuflen = 0;
+       }
+
+       /*
+        * Allocate a buffer to do temporary reads into, before we
+        * call sasl_decode()
+        */
+
+       nsc->sasl_tmpbuf = mh_xmalloc(nsc->sasl_maxbufsize);
+
+       /*
+        * Okay, this is a bit weird.  Make sure that the input buffer
+        * is at least TWICE the size of the max buffer size.  That's
+        * because if we're consuming data but want to extend the current
+        * buffer, we want to be sure there's room for another full buffer's
+        * worth of data.
+        */
+
+       if (nsc->ns_inbufsize < nsc->sasl_maxbufsize * 2) {
+           size_t offset = nsc->ns_inptr - nsc->ns_inbuffer;
+           nsc->ns_inbufsize = nsc->sasl_maxbufsize * 2;
+           nsc->ns_inbuffer = mh_xrealloc(nsc->ns_inbuffer, nsc->ns_inbufsize);
+           nsc->ns_inptr = nsc->ns_inbuffer + offset;
+       }
+
+       nsc->sasl_seclayer = 1;
+    }
 
     return OK;
 #else
-    *errstr = netsec_errstr("SASL not supported");
+    /*
+     * If we're at this point, then either we have NEITHER OAuth2 or
+     * Cyrus-SASL compiled in, or have OAuth2 but didn't give the XOAUTH2
+     * mechanism on the command line.
+     */
+
+    if (! nsc->sasl_mech)
+       netsec_err(errstr, "SASL library support not available; please "
+                  "specify a SASL mechanism to use");
+    else
+       netsec_err(errstr, "No support for the %s SASL mechanism",
+                  nsc->sasl_mech);
 
     return NOTOK;
 #endif /* CYRUS_SASL */
@@ -419,11 +1406,24 @@ netsec_negotiate_sasl(netsec_context *nsc, const char *mechlist, char **errstr)
 char *
 netsec_get_sasl_mechanism(netsec_context *nsc)
 {
-#ifdef CYRUS_SASL
     return nsc->sasl_chosen_mech;
-#else /* CYRUS_SASL */
-    return NULL;
-#endif /* CYRUS_SASL */
+}
+
+/*
+ * Set an OAuth2 service name, if we support it.
+ */
+
+int
+netsec_set_oauth_service(netsec_context *nsc, const char *service)
+{
+#ifdef OAUTH_SUPPORT
+    nsc->oauth_service = getcpy(service);
+    return OK;
+#else /* OAUTH_SUPPORT */
+    NMH_UNUSED(nsc);
+    NMH_UNUSED(service);
+    return NOTOK;
+#endif /* OAUTH_SUPPORT */
 }
 
 /*
@@ -431,12 +1431,12 @@ netsec_get_sasl_mechanism(netsec_context *nsc)
  */
 
 int
-netsec_set_tls(netsec_context *nsc, int tls, char **errstr)
+netsec_set_tls(netsec_context *nsc, int tls, int noverify, char **errstr)
 {
-    if (tls) {
 #ifdef TLS_SUPPORT
+    if (tls) {
        SSL *ssl;
-       BIO *sbio, *ssl_bio;;
+        BIO *rbio, *wbio, *ssl_bio;
 
        if (! tls_initialized) {
            SSL_library_init();
@@ -451,21 +1451,26 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr)
            sslctx = SSL_CTX_new(SSLv23_client_method());
 
            if (! sslctx) {
-               *errstr = netsec_errstr("Unable to initialize OpenSSL "
-                                       "context: %s",
-                                       ERR_error_string(ERR_get_error(), NULL));
+               netsec_err(errstr, "Unable to initialize OpenSSL context: %s",
+                          ERR_error_string(ERR_get_error(), NULL));
                return NOTOK;
            }
 
            SSL_CTX_set_options(sslctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 |
                                SSL_OP_NO_TLSv1);
 
+           if (!SSL_CTX_set_default_verify_paths(sslctx)) {
+               netsec_err(errstr, "Unable to set default certificate "
+                          "verification paths: %s",
+                          ERR_error_string(ERR_get_error(), NULL));
+               return NOTOK;
+           }
+
            tls_initialized++;
        }
 
-       if (nsc->ns_fd == -1) {
-           *errstr = netsec_errstr("Invalid file descriptor in netsec "
-                                   "context");
+       if (nsc->ns_readfd == -1 || nsc->ns_writefd == -1) {
+           netsec_err(errstr, "Invalid file descriptor in netsec context");
            return NOTOK;
        }
 
@@ -477,11 +1482,17 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr)
        ssl = SSL_new(sslctx);
 
        if (! ssl) {
-           *errstr = netsec_errstr("Unable to create SSL connection: %s",
-                                   ERR_error_string(ERR_get_error(), NULL));
+           netsec_err(errstr, "Unable to create SSL connection: %s",
+                      ERR_error_string(ERR_get_error(), NULL));
            return NOTOK;
        }
 
+       /*
+        * Never bother us, since we are using blocking sockets.
+        */
+
+       SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
+
        /*
         * This is a bit weird, so pay attention.
         *
@@ -490,60 +1501,106 @@ netsec_set_tls(netsec_context *nsc, int tls, char **errstr)
         * supplied socket.
         *
         * Then we create an SSL BIO, and assign our current SSL connection
-        * to it.  We then create a buffer BIO and push it in front of our
-        * SSL BIO.  So the chain looks like:
-        *
-        * buffer BIO -> SSL BIO -> socket BIO.
+        * to it.  This is done so our code stays simple if we want to use
+        * any buffering BIOs (right now we do our own buffering).
+        * So the chain looks like:
         *
-        * So writes and reads are buffered (we mostly care about writes).
+        * SSL BIO -> socket BIO.
         */
 
-       sbio = BIO_new_socket(nsc->ns_fd, BIO_NOCLOSE);
+       rbio = BIO_new_socket(nsc->ns_readfd, BIO_CLOSE);
 
-       if (! sbio) {
-           *errstr = netsec_errstr("Unable to create a socket BIO: %s",
-                                   ERR_error_string(ERR_get_error(), NULL));
+       if (! rbio) {
+           netsec_err(errstr, "Unable to create a read socket BIO: %s",
+                      ERR_error_string(ERR_get_error(), NULL));
            SSL_free(ssl);
            return NOTOK;
        }
 
-       SSL_set_bio(ssl, sbio, sbio);
-       SSL_set_connect_state(ssl);
-
-       nsc->ssl_io = BIO_new(BIO_f_buffer());
+       wbio = BIO_new_socket(nsc->ns_writefd, BIO_CLOSE);
 
-       if (! nsc->ssl_io) {
-           *errstr = netsec_errstr("Unable to create a buffer BIO: %s",
-                                   ERR_error_string(ERR_get_error(), NULL));
+       if (! wbio) {
+           netsec_err(errstr, "Unable to create a write socket BIO: %s",
+                      ERR_error_string(ERR_get_error(), NULL));
            SSL_free(ssl);
+           BIO_free(rbio);
            return NOTOK;
        }
 
+       SSL_set_bio(ssl, rbio, wbio);
+       SSL_set_connect_state(ssl);
+
+       /*
+        * If noverify is NOT set, then do certificate validation.
+        * Turning on SSL_VERIFY_PEER will verify the certificate chain
+        * against locally stored root certificates (the locations are
+        * set using SSL_CTX_set_default_verify_paths()), and we put
+        * the hostname in the X509 verification parameters so the OpenSSL
+        * code will verify that the hostname appears in the server
+        * certificate.
+        */
+
+       if (! noverify) {
+#ifdef HAVE_X509_VERIFY_PARAM_SET1_HOST
+           X509_VERIFY_PARAM *param;
+#endif /* HAVE_X509_VERIFY_PARAM_SET1_HOST */
+
+           SSL_set_verify(ssl, SSL_VERIFY_PEER, NULL);
+           if (! nsc->ns_hostname) {
+               netsec_err(errstr, "Internal error: hostname not set and "
+                          "certification verification enabled");
+               SSL_free(ssl);
+               return NOTOK;
+           }
+
+#ifdef HAVE_X509_VERIFY_PARAM_SET1_HOST
+           param = SSL_get0_param(ssl);
+
+           if (! X509_VERIFY_PARAM_set1_host(param, nsc->ns_hostname, 0)) {
+               netsec_err(errstr, "Unable to add hostname %s to cert "
+                          "verification parameters: %s", nsc->ns_hostname,
+                          ERR_error_string(ERR_get_error(), NULL));
+               SSL_free(ssl);
+               return NOTOK;
+           }
+#endif /* HAVE_X509_VERIFY_PARAM_SET1_HOST */
+       }
+
        ssl_bio = BIO_new(BIO_f_ssl());
 
        if (! ssl_bio) {
-           *errstr = netsec_errstr("Unable to create a SSL BIO: %s",
-                                   ERR_error_string(ERR_get_error(), NULL));
+           netsec_err(errstr, "Unable to create a SSL BIO: %s",
+                      ERR_error_string(ERR_get_error(), NULL));
            SSL_free(ssl);
            return NOTOK;
        }
 
        BIO_set_ssl(ssl_bio, ssl, BIO_CLOSE);
-       BIO_push(nsc->ssl_io, ssl_bio);
+       nsc->ssl_io = ssl_bio;
 
-       return OK;
-    } else {
-       BIO_free_all(nsc->ssl_io);
-       nsc->ssl_io = NULL;
+       /*
+        * Since SSL now owns these file descriptors, have it handle the
+        * closing of them instead of netsec_shutdown().
+        */
+
+       nsc->ns_noclose = 1;
 
        return OK;
     }
+    BIO_free_all(nsc->ssl_io);
+    nsc->ssl_io = NULL;
+
 #else /* TLS_SUPPORT */
-       *errstr = netsec_errstr("TLS is not supported");
+    NMH_UNUSED(nsc);
+    NMH_UNUSED(noverify);
 
+    if (tls) {
+       netsec_err(errstr, "TLS is not supported");
        return NOTOK;
     }
 #endif /* TLS_SUPPORT */
+
+    return OK;
 }
 
 /*
@@ -555,14 +1612,47 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr)
 {
 #ifdef TLS_SUPPORT
     if (! nsc->ssl_io) {
-       *errstr = netsec_errstr("TLS has not been configured for this "
-                               "connection");
+       netsec_err(errstr, "TLS has not been configured for this connection");
        return NOTOK;
     }
 
     if (BIO_do_handshake(nsc->ssl_io) < 1) {
-       *errstr = netsec_errstr("TLS negotiation failed: %s",
-                               ERR_error_string(ERR_get_error(), NULL));
+       unsigned long errcode = ERR_get_error();
+
+       /*
+        * Print a more detailed message if it was certificate verification
+        * failure.
+        */
+
+       if (ERR_GET_LIB(errcode) == ERR_LIB_SSL &&
+               ERR_GET_REASON(errcode) == SSL_R_CERTIFICATE_VERIFY_FAILED) {
+           SSL *ssl;
+
+           if (BIO_get_ssl(nsc->ssl_io, &ssl) < 1) {
+               netsec_err(errstr, "Certificate verification failed, but "
+                          "cannot retrieve SSL handle: %s",
+                           ERR_error_string(errcode, NULL));
+           } else {
+               netsec_err(errstr, "Server certificate verification failed: %s",
+                          X509_verify_cert_error_string(
+                                               SSL_get_verify_result(ssl)));
+           }
+       } else {
+           netsec_err(errstr, "TLS negotiation failed: %s",
+                       ERR_error_string(errcode, NULL));
+       }
+
+       /*
+        * Because negotiation failed, shut down TLS so we don't get any
+        * garbage on the connection.  Because of weirdness with SSL_shutdown,
+        * we end up calling it twice: once explicitly, once as part of
+        * BIO_free_all().
+        */
+
+       BIO_ssl_shutdown(nsc->ssl_io);
+       BIO_free_all(nsc->ssl_io);
+       nsc->ssl_io = NULL;
+
        return NOTOK;
     }
 
@@ -573,10 +1663,11 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr)
            fprintf(stderr, "WARNING: cannot determine SSL ciphers\n");
        } else {
            const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl);
-           fprintf(stderr, "TLS negotation successful: %s(%d) %s\n",
+           fprintf(stderr, "TLS negotiation successful: %s(%d) %s\n",
                    SSL_CIPHER_get_name(cipher),
                    SSL_CIPHER_get_bits(cipher, NULL),
                    SSL_CIPHER_get_version(cipher));
+           SSL_SESSION_print_fp(stderr, SSL_get_session(ssl));
        }
     }
 
@@ -584,7 +1675,8 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr)
 
     return OK;
 #else /* TLS_SUPPORT */
-    *errstr = netsec_errstr("TLS not supported");
+    NMH_UNUSED(nsc);
+    netsec_err(errstr, "TLS not supported");
 
     return NOTOK;
 #endif /* TLS_SUPPORT */
@@ -594,14 +1686,17 @@ netsec_negotiate_tls(netsec_context *nsc, char **errstr)
  * Generate an (allocated) error string
  */
 
-static char *
-netsec_errstr(const char *fmt, ...)
+void
+netsec_err(char **errstr, const char *fmt, ...)
 {
     va_list ap;
     size_t errbufsize;
     char *errbuf = NULL;
     int rc = 127;
 
+    if (! errstr)
+       return;
+
     do {
        errbufsize = rc + 1;
        errbuf = mh_xrealloc(errbuf, errbufsize);
@@ -610,5 +1705,5 @@ netsec_errstr(const char *fmt, ...)
        va_end(ap);
     } while (rc >= (int) errbufsize);
 
-    return errbuf;
+    *errstr = errbuf;
 }