+ 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);
+}
+
+/*
+ * Set the file descriptor for our context
+ */
+
+void
+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)
+{
+ 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 */
+}
+
+/*
+ * Fill our read buffer with some data from the network.
+ */
+
+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;