From b028ad841f506e093a09c88918f8c2e04ef4cbd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BChler?= Date: Mon, 20 May 2013 13:46:16 +0200 Subject: [PATCH] [mod_gnutls] stream --- src/CMakeLists.txt | 6 +- src/modules/Makefile.am | 3 +- src/modules/gnutls_filter.c | 616 ++++++++++++++++++++++++++++++++++++ src/modules/gnutls_filter.h | 29 ++ src/modules/mod_gnutls.c | 375 +++++++--------------- 5 files changed, 769 insertions(+), 260 deletions(-) create mode 100644 src/modules/gnutls_filter.c create mode 100644 src/modules/gnutls_filter.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9846697..17c078b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -368,9 +368,9 @@ IF(WITH_LUA) ENDIF(WITH_LUA) IF(WITH_GNUTLS) -# ADD_AND_INSTALL_LIBRARY(mod_gnutls "modules/mod_gnutls.c") -# TARGET_LINK_LIBRARIES(mod_gnutls ${GNUTLS_LDFLAGS}) -# ADD_TARGET_PROPERTIES(mod_gnutls COMPILE_FLAGS ${GNUTLS_CFLAGS}) + ADD_AND_INSTALL_LIBRARY(mod_gnutls "modules/mod_gnutls.c;modules/gnutls_filter.c") + TARGET_LINK_LIBRARIES(mod_gnutls ${GNUTLS_LDFLAGS}) + ADD_TARGET_PROPERTIES(mod_gnutls COMPILE_FLAGS ${GNUTLS_CFLAGS}) ENDIF(WITH_GNUTLS) IF(WITH_OPENSSL) diff --git a/src/modules/Makefile.am b/src/modules/Makefile.am index 1e2eede..86c3a36 100644 --- a/src/modules/Makefile.am +++ b/src/modules/Makefile.am @@ -77,10 +77,11 @@ libmod_fortune_la_LIBADD = $(common_libadd) if USE_GNUTLS install_libs += libmod_gnutls.la libmod_gnutls_la_CPPFLAGS = $(AM_CPPFLAGS) $(GNUTLS_CFLAGS) -libmod_gnutls_la_SOURCES = mod_gnutls.c +libmod_gnutls_la_SOURCES = mod_gnutls.c gnutls_filter.c libmod_gnutls_la_LDFLAGS = $(common_ldflags) libmod_gnutls_la_LIBADD = $(common_libadd) $(GNUTLS_LIBS) endif +EXTRA_DIST += gnutls_filter.h install_libs += libmod_limit.la libmod_limit_la_SOURCES = mod_limit.c diff --git a/src/modules/gnutls_filter.c b/src/modules/gnutls_filter.c new file mode 100644 index 0000000..ae8ed95 --- /dev/null +++ b/src/modules/gnutls_filter.c @@ -0,0 +1,616 @@ + +#include "gnutls_filter.h" + +struct liGnuTLSFilter { + int refcount; + const liGnuTLSFilterCallbacks *callbacks; + gpointer callback_data; + + liServer *srv; + liWorker *wrk; + liLogContext *log_context; /* TODO: support setting this to VR context */ + + gnutls_session_t session; + liStream crypt_source; + liStream crypt_drain; + liStream plain_source; + liStream plain_drain; + + liBuffer *raw_in_buffer; /* for gnutls_record_recv */ + liBuffer *raw_out_buffer; /* stream_pushv */ + + unsigned int initial_handshaked_finished:1; + unsigned int closing:1, aborted:1; + unsigned int write_wants_read:1; +}; + +static ssize_t stream_push(gnutls_transport_ptr_t, const void*, size_t); +static ssize_t stream_pushv(gnutls_transport_ptr_t, const giovec_t * iov, int iovcnt); +static ssize_t stream_pull(gnutls_transport_ptr_t, void*, size_t); + +static ssize_t stream_push(gnutls_transport_ptr_t trans, const void *buf, size_t len) { + giovec_t vec; + vec.iov_base = (void *) buf; + vec.iov_len = len; + return stream_pushv(trans, &vec, 1); +} +static ssize_t stream_pushv(gnutls_transport_ptr_t trans, const giovec_t * iov, int iovcnt) { + const ssize_t blocksize = 16*1024; /* 16k */ + liGnuTLSFilter *f = (liGnuTLSFilter*) trans; + liChunkQueue *cq; + int i; + liBuffer *buf; + gboolean cq_buf_append; + ssize_t written = 0; + + errno = ECONNRESET; + + if (NULL == f || NULL == f->crypt_source.out) return -1; + cq = f->crypt_source.out; + if (cq->is_closed) return -1; + + buf = f->raw_out_buffer; + cq_buf_append = (buf != NULL && buf == li_chunkqueue_get_last_buffer(cq, 1024)); + for (i = 0; i < iovcnt; ++i) { + const char *data = iov[i].iov_base; + size_t len = iov[i].iov_len; + + while (len > 0) { + size_t bufsize, do_write; + if (NULL == buf) buf = li_buffer_new(blocksize); + + bufsize = buf->alloc_size - buf->used; + do_write = (bufsize > len) ? len : bufsize; + memcpy(buf->addr + buf->used, data, do_write); + len -= do_write; + data += do_write; + if (cq_buf_append) { + /* also updates buf->used */ + li_chunkqueue_update_last_buffer_size(cq, do_write); + } else { + gsize offset = buf->used; + buf->used += do_write; + li_buffer_acquire(buf); + li_chunkqueue_append_buffer2(cq, buf, offset, do_write); + cq_buf_append = TRUE; + } + if (buf->used == buf->alloc_size) { + li_buffer_release(buf); + buf = NULL; + cq_buf_append = FALSE; + } + written += do_write; + } + } + if (NULL != buf && buf->alloc_size - buf->used < 1024) { + li_buffer_release(buf); + f->raw_out_buffer = buf = NULL; + } else { + f->raw_out_buffer = buf; + } + + li_stream_notify_later(&f->crypt_source); + + errno = 0; + return written; +} +static ssize_t stream_pull(gnutls_transport_ptr_t trans, void *buf, size_t len) { + liGnuTLSFilter *f = (liGnuTLSFilter*) trans; + liChunkQueue *cq; + + errno = ECONNRESET; + if (NULL == f || NULL == f->crypt_drain.out) return -1; + + cq = f->crypt_drain.out; + + if (0 == cq->length) { + if (cq->is_closed) { + errno = 0; + return 0; + } else { + errno = EAGAIN; + return -1; + } + } + + if (len > (size_t) cq->length) len = cq->length; + if (!li_chunkqueue_extract_to_memory(cq, len, buf, NULL)) return -1; + li_chunkqueue_skip(cq, len); + + errno = 0; + return len; +} + +static void f_close_gnutls(liGnuTLSFilter *f) { + if (NULL != f->session && !f->closing) { + liCQLimit *limit; + f->closing = TRUE; + f->session = NULL; + + assert(NULL != f->crypt_source.out); + assert(NULL != f->crypt_source.out->limit); + limit = f->crypt_source.out->limit; + limit->notify = NULL; + limit->context = NULL; + + li_stream_disconnect(&f->plain_source); + + li_stream_disconnect(&f->plain_drain); + li_stream_disconnect_dest(&f->plain_source); + + f->log_context = NULL; + if (NULL != f->callbacks && NULL != f->callbacks->closed_cb) { + f->callbacks->closed_cb(f, f->callback_data); + } + } +} +static void f_acquire(liGnuTLSFilter *f) { + assert(f->refcount > 0); + ++f->refcount; +} +static void f_release(liGnuTLSFilter *f) { + assert(f->refcount > 0); + if (0 == --f->refcount) { + f->refcount = 1; + f_close_gnutls(f); + + g_slice_free(liGnuTLSFilter, f); + } +} +static void f_abort_gnutls(liGnuTLSFilter *f) { + if (f->aborted) return; + f->aborted = TRUE; + f_acquire(f); + f_close_gnutls(f); + li_stream_disconnect(&f->crypt_drain); + li_stream_disconnect_dest(&f->crypt_source); + f_release(f); +} + + +static void do_handle_error(liGnuTLSFilter *f, const char *gnutlsfunc, int r, gboolean writing) { + switch (r) { + case GNUTLS_E_AGAIN: + if (writing) f->write_wants_read = TRUE; + break; + case GNUTLS_E_REHANDSHAKE: + if (f->initial_handshaked_finished) { + _ERROR(f->srv, f->wrk, f->log_context, "%s: gnutls: client initiated renegotitation, closing connection", gnutlsfunc); + f_abort_gnutls(f); + } + break; + case GNUTLS_E_UNEXPECTED_PACKET_LENGTH: + f_abort_gnutls(f); + break; + case GNUTLS_E_UNKNOWN_CIPHER_SUITE: + case GNUTLS_E_UNSUPPORTED_VERSION_PACKET: + _DEBUG(f->srv, f->wrk, f->log_context, "%s (%s): %s", gnutlsfunc, + gnutls_strerror_name(r), gnutls_strerror(r)); + f_abort_gnutls(f); + break; + default: + if (gnutls_error_is_fatal(r)) { + _ERROR(f->srv, f->wrk, f->log_context, "%s (%s): %s", gnutlsfunc, + gnutls_strerror_name(r), gnutls_strerror(r)); + f_abort_gnutls(f); + } else { + _ERROR(f->srv, f->wrk, f->log_context, "%s non fatal (%s): %s", gnutlsfunc, + gnutls_strerror_name(r), gnutls_strerror(r)); + } + } +} + +#define TIMEDIFF_MS(ts1, ts2) (((ts2).tv_sec - (ts1).tv_sec)*1.0e3 + ((ts2).tv_nsec - (ts1).tv_nsec)*1.0e-6) + +static gboolean do_gnutls_handshake(liGnuTLSFilter *f, gboolean writing) { + int r; + + assert(!f->initial_handshaked_finished); + + r = gnutls_handshake(f->session); + if (GNUTLS_E_SUCCESS == r) { + f->initial_handshaked_finished = 1; + li_stream_acquire(&f->plain_source); + li_stream_acquire(&f->plain_drain); + f->callbacks->handshake_cb(f, f->callback_data, &f->plain_source, &f->plain_drain); + li_stream_release(&f->plain_source); + li_stream_release(&f->plain_drain); + return TRUE; + } else { + do_handle_error(f, "gnutls_handshake", r, writing); + return FALSE; + } +} + +static void do_gnutls_read(liGnuTLSFilter *f) { + const ssize_t blocksize = 16*1024; /* 16k */ + off_t max_read = 4 * blocksize; /* 64k */ + ssize_t r; + off_t len = 0; + liChunkQueue *cq = f->plain_source.out; + + f_acquire(f); + + if (NULL != f->session && !f->initial_handshaked_finished && !do_gnutls_handshake(f, FALSE)) goto out; + if (NULL == f->session) { + f_abort_gnutls(f); + goto out; + } + + do { + liBuffer *buf; + gboolean cq_buf_append; + + buf = li_chunkqueue_get_last_buffer(cq, 1024); + cq_buf_append = (buf != NULL); + + if (buf != NULL) { + /* use last buffer as raw_in_buffer; they should be the same anyway */ + if (G_UNLIKELY(buf != f->raw_in_buffer)) { + li_buffer_acquire(buf); + li_buffer_release(f->raw_in_buffer); + f->raw_in_buffer = buf; + } + } else { + buf = f->raw_in_buffer; + if (buf != NULL && buf->alloc_size - buf->used < 1024) { + /* release *buffer */ + li_buffer_release(buf); + f->raw_in_buffer = buf = NULL; + } + if (buf == NULL) { + f->raw_in_buffer = buf = li_buffer_new(blocksize); + } + } + assert(f->raw_in_buffer == buf); + + r = gnutls_record_recv(f->session, buf->addr + buf->used, buf->alloc_size - buf->used); + if (r < 0) { + do_handle_error(f, "gnutls_record_recv", r, FALSE); + goto out; + } else if (r == 0) { + /* clean shutdown? */ + f->plain_source.out->is_closed = TRUE; + f->plain_drain.out->is_closed = TRUE; + f->crypt_source.out->is_closed = TRUE; + f->crypt_drain.out->is_closed = TRUE; + li_stream_disconnect(&f->crypt_drain); + li_stream_disconnect_dest(&f->crypt_source); + f_close_gnutls(f); + goto out; + } + + if (cq_buf_append) { + li_chunkqueue_update_last_buffer_size(cq, r); + } else { + gsize offset; + + li_buffer_acquire(buf); + + offset = buf->used; + buf->used += r; + li_chunkqueue_append_buffer2(cq, buf, offset, r); + } + if (buf->alloc_size - buf->used < 1024) { + /* release *buffer */ + li_buffer_release(buf); + f->raw_in_buffer = buf = NULL; + } + len += r; + } while (len < max_read); + +out: + f_release(f); +} + +#if GNUTLS_VERSION_NUMBER >= 0x030109 +/* gnutls_record_cork / gnutls_record_uncork available since 3.1.9 */ +#define USE_CORK +#endif + +static void do_gnutls_write(liGnuTLSFilter *f) { + const ssize_t blocksize = 16*1024; /* 16k */ + char *block_data; + off_t block_len; + ssize_t r; + off_t write_max; +#ifdef USE_CORK + gboolean corked = FALSE; +#endif + liChunkQueue *cq = f->plain_drain.out; + + f_acquire(f); + + f->write_wants_read = FALSE; + + /* use space in (encrypted) outgoing buffer as amounts of bytes we try to write from (plain) output + * don't care if we write a little bit more than the limit allowed */ + write_max = li_chunkqueue_limit_available(f->crypt_source.out); + assert(write_max >= 0); /* we set a limit! */ + if (0 == write_max) goto out; + /* if we start writing, try to write at least blocksize bytes */ + if (write_max < blocksize) write_max = blocksize; + + if (NULL != f->session && !f->initial_handshaked_finished && !do_gnutls_handshake(f, TRUE)) goto out; + if (NULL == f->session) { + f_abort_gnutls(f); + goto out; + } + +#ifdef USE_CORK + if (0 != cq->length && cq->queue.length > 1) { + corked = TRUE; + gnutls_record_cork(f->session); + } +#endif + + do { + GError *err = NULL; + liChunkIter ci; + + if (0 == cq->length) break; + + ci = li_chunkqueue_iter(cq); + switch (li_chunkiter_read(ci, 0, blocksize, &block_data, &block_len, &err)) { + case LI_HANDLER_GO_ON: + break; + case LI_HANDLER_ERROR: + if (NULL != err) { + _ERROR(f->srv, f->wrk, f->log_context, "Couldn't read data from chunkqueue: %s", err->message); + g_error_free(err); + } + /* fall through */ + default: + f_abort_gnutls(f); + goto out; + } + + r = gnutls_record_send(f->session, block_data, block_len); + if (r <= 0) { + do_handle_error(f, "gnutls_record_send", r, TRUE); + goto out; + } + + li_chunkqueue_skip(cq, r); + write_max -= r; + } while (r == block_len && write_max > 0); + + if (cq->is_closed && 0 == cq->length) { + r = gnutls_bye(f->session, GNUTLS_SHUT_RDWR); + switch (r) { + case GNUTLS_E_SUCCESS: + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + f->plain_source.out->is_closed = TRUE; + f->crypt_source.out->is_closed = TRUE; + f->crypt_drain.out->is_closed = TRUE; + f_close_gnutls(f); + break; + default: + do_handle_error(f, "gnutls_bye", r, TRUE); + f_abort_gnutls(f); + break; + } + } else if (0 < cq->length && 0 != li_chunkqueue_limit_available(f->crypt_source.out)) { + li_stream_again_later(&f->plain_drain); + } + +out: +#ifdef USE_CORK + if (NULL != f->session && corked) { + corked = TRUE; + gnutls_record_uncork(f->session); + } +#endif + + f_release(f); +} + +/* ssl crypted out -> io */ +static void stream_crypt_source_cb(liStream *stream, liStreamEvent event) { + liGnuTLSFilter *f = LI_CONTAINER_OF(stream, liGnuTLSFilter, crypt_source); + switch (event) { + case LI_STREAM_NEW_DATA: + /* data comes through SSL */ + break; + case LI_STREAM_NEW_CQLIMIT: + break; + case LI_STREAM_CONNECTED_DEST: /* io out */ + break; + case LI_STREAM_CONNECTED_SOURCE: /* plain_drain */ + break; + case LI_STREAM_DISCONNECTED_DEST: /* io out disconnect */ + if (!stream->out->is_closed || 0 != stream->out->length) { + f_abort_gnutls(f); /* didn't read everything */ + } + break; + case LI_STREAM_DISCONNECTED_SOURCE: /* plain_drain */ + if (!stream->out->is_closed) { /* f_close_ssl before we were ready */ + f_abort_gnutls(f); + } + break; + case LI_STREAM_DESTROY: + f_release(f); + break; + } +} +/* io -> ssl crypted in */ +static void stream_crypt_drain_cb(liStream *stream, liStreamEvent event) { + liGnuTLSFilter *f = LI_CONTAINER_OF(stream, liGnuTLSFilter, crypt_drain); + switch (event) { + case LI_STREAM_NEW_DATA: + if (!stream->out->is_closed && NULL != stream->source) { + li_chunkqueue_steal_all(stream->out, stream->source->out); + stream->out->is_closed = stream->out->is_closed || stream->source->out->is_closed; + li_stream_notify(stream); /* tell plain_source to do SSL_read */ + } + if (stream->out->is_closed) { + li_stream_disconnect(stream); + } + break; + case LI_STREAM_NEW_CQLIMIT: + break; + case LI_STREAM_CONNECTED_DEST: /* plain_source */ + break; + case LI_STREAM_CONNECTED_SOURCE: /* io in */ + break; + case LI_STREAM_DISCONNECTED_DEST: /* plain_source */ + if (!stream->out->is_closed || 0 != stream->out->length) { + f_abort_gnutls(f); /* didn't read everything */ + } + break; + case LI_STREAM_DISCONNECTED_SOURCE: /* io in disconnect */ + if (!stream->out->is_closed) { + f_abort_gnutls(f); /* conn aborted */ + } + break; + case LI_STREAM_DESTROY: + f_release(f); + break; + } +} +/* ssl (plain) -> app */ +static void stream_plain_source_cb(liStream *stream, liStreamEvent event) { + liGnuTLSFilter *f = LI_CONTAINER_OF(stream, liGnuTLSFilter, plain_source); + switch (event) { + case LI_STREAM_NEW_DATA: + do_gnutls_read(f); + if (f->write_wants_read) do_gnutls_write(f); + li_stream_notify(stream); + break; + case LI_STREAM_NEW_CQLIMIT: + break; + case LI_STREAM_CONNECTED_DEST: /* app */ + break; + case LI_STREAM_CONNECTED_SOURCE: /* crypt_drain */ + break; + case LI_STREAM_DISCONNECTED_DEST: /* app */ + if (!stream->out->is_closed || 0 != stream->out->length) { + f_abort_gnutls(f); /* didn't read everything */ + } + break; + case LI_STREAM_DISCONNECTED_SOURCE: /* crypt_drain */ + if (!stream->out->is_closed) { + f_abort_gnutls(f); /* didn't get everything */ + } + break; + case LI_STREAM_DESTROY: + f_release(f); + break; + } +} +/* app -> ssl (plain) */ +static void stream_plain_drain_cb(liStream *stream, liStreamEvent event) { + liGnuTLSFilter *f = LI_CONTAINER_OF(stream, liGnuTLSFilter, plain_drain); + switch (event) { + case LI_STREAM_NEW_DATA: + if (!stream->out->is_closed && NULL != stream->source) { + li_chunkqueue_steal_all(stream->out, stream->source->out); + stream->out->is_closed = stream->out->is_closed || stream->source->out->is_closed; + } + do_gnutls_write(f); + if (stream->out->is_closed) { + li_stream_disconnect(stream); + stream->out->is_closed = FALSE; + } + break; + case LI_STREAM_NEW_CQLIMIT: + break; + case LI_STREAM_CONNECTED_DEST: /* crypt_source */ + break; + case LI_STREAM_CONNECTED_SOURCE: /* app */ + break; + case LI_STREAM_DISCONNECTED_DEST: + if (!stream->out->is_closed || 0 != stream->out->length) { + f_abort_gnutls(f); /* didn't read everything */ + } + break; + case LI_STREAM_DISCONNECTED_SOURCE: + if (!stream->out->is_closed) { + f_abort_gnutls(f); /* didn't get everything */ + } + break; + case LI_STREAM_DESTROY: + f_release(f); + break; + } +} +static void stream_crypt_source_limit_notify_cb(gpointer context, gboolean locked) { + liGnuTLSFilter *f = context; + if (!locked) li_stream_again_later(&f->plain_drain); +} + +static int post_client_hello_cb(gnutls_session_t session) { + liGnuTLSFilter *f = gnutls_session_get_ptr(session); + return f->callbacks->post_client_hello_cb(f, f->callback_data); +} + +liGnuTLSFilter* li_gnutls_filter_new( + liServer *srv, liWorker *wrk, + const liGnuTLSFilterCallbacks *callbacks, gpointer data, + gnutls_session_t session, liStream *crypt_source, liStream *crypt_drain +) { + liEventLoop *loop = crypt_source->loop; + liGnuTLSFilter *f; + liCQLimit *out_limit; + + f = g_slice_new0(liGnuTLSFilter); + f->refcount = 5; /* 1 + 4 streams */ + f->callbacks = callbacks; + f->callback_data = data; + f->srv = srv; + f->wrk = wrk; + + f->session = session; + gnutls_transport_set_ptr(f->session, (gnutls_transport_ptr_t) f); + gnutls_transport_set_push_function(f->session, stream_push); + gnutls_transport_set_vec_push_function(f->session, stream_pushv); + gnutls_transport_set_pull_function(f->session, stream_pull); + + gnutls_session_set_ptr(f->session, f); + gnutls_handshake_set_post_client_hello_function(f->session, post_client_hello_cb); + + f->initial_handshaked_finished = 0; + f->closing = f->aborted = 0; + f->write_wants_read = 0; + + li_stream_init(&f->crypt_source, loop, stream_crypt_source_cb); + li_stream_init(&f->crypt_drain, loop, stream_crypt_drain_cb); + li_stream_init(&f->plain_source, loop, stream_plain_source_cb); + li_stream_init(&f->plain_drain, loop, stream_plain_drain_cb); + + /* "virtual" connections - the content goes through SSL */ + li_stream_connect(&f->plain_drain, &f->crypt_source); + li_stream_connect(&f->crypt_drain, &f->plain_source); + + li_stream_connect(crypt_source, &f->crypt_drain); + li_stream_connect(&f->crypt_source, crypt_drain); + + /* separate limit for buffer of encrypted data + * + * f->plain_drain is already connected to f->crypt_source, + * so they won't share the same limit */ + out_limit = li_cqlimit_new(); + out_limit->notify = stream_crypt_source_limit_notify_cb; + out_limit->context = f; + li_cqlimit_set_limit(out_limit, 32*1024); + li_chunkqueue_set_limit(crypt_drain->out, out_limit); + li_chunkqueue_set_limit(f->crypt_source.out, out_limit); + li_cqlimit_release(out_limit); + + return f; +} + +void li_gnutls_filter_free(liGnuTLSFilter *f) { + assert(NULL != f->callbacks); + f->callbacks = NULL; + f->callback_data = NULL; + + f_close_gnutls(f); + + li_stream_release(&f->crypt_source); + li_stream_release(&f->crypt_drain); + li_stream_release(&f->plain_source); + li_stream_release(&f->plain_drain); + f_release(f); +} diff --git a/src/modules/gnutls_filter.h b/src/modules/gnutls_filter.h new file mode 100644 index 0000000..8d8c8c9 --- /dev/null +++ b/src/modules/gnutls_filter.h @@ -0,0 +1,29 @@ +#ifndef _LIGHTTPD_GNUTLS_FILTER_H_ +#define _LIGHTTPD_GNUTLS_FILTER_H_ + +#include + +#include + +typedef struct liGnuTLSFilter liGnuTLSFilter; + +typedef void (*liGnuTLSFilterHandshakeCB)(liGnuTLSFilter *f, gpointer data, liStream *plain_source, liStream *plain_drain); +typedef void (*liGnuTLSFilterClosedCB)(liGnuTLSFilter *f, gpointer data); +typedef int (*liGnuTLSFilterPostClientHelloCB)(liGnuTLSFilter *f, gpointer data); + +typedef struct liGnuTLSFilterCallbacks liGnuTLSFilterCallbacks; +struct liGnuTLSFilterCallbacks { + liGnuTLSFilterHandshakeCB handshake_cb; /* called after initial handshake is done */ + liGnuTLSFilterClosedCB closed_cb; + liGnuTLSFilterPostClientHelloCB post_client_hello_cb; +}; + +LI_API liGnuTLSFilter* li_gnutls_filter_new( + liServer *srv, liWorker *wrk, + const liGnuTLSFilterCallbacks *callbacks, gpointer data, + gnutls_session_t session, liStream *crypt_source, liStream *crypt_drain); + +/* doesn't call closed_cb; but you can call this from closed_cb */ +LI_API void li_gnutls_filter_free(liGnuTLSFilter *f); + +#endif diff --git a/src/modules/mod_gnutls.c b/src/modules/mod_gnutls.c index 7f6551b..4cde424 100644 --- a/src/modules/mod_gnutls.c +++ b/src/modules/mod_gnutls.c @@ -1,6 +1,8 @@ #include +#include "gnutls_filter.h" + #include #include @@ -17,13 +19,10 @@ struct mod_connection_ctx { liConnection *con; mod_context *ctx; - int con_events; - liJob con_handle_events_job; - - unsigned int gnutls_again_in_progress:1; + liGnuTLSFilter *tls_filter; - unsigned int initial_handshaked_finished:1; - unsigned int client_initiated_renegotiation:1; + liIOStream *sock_stream; + gpointer simple_socket_data; }; struct mod_context { @@ -92,306 +91,175 @@ error0: return NULL; } +static void tcp_io_cb(liIOStream *stream, liIOStreamEvent event) { + mod_connection_ctx *conctx = stream->data; + assert(NULL == conctx->sock_stream || conctx->sock_stream == stream); + if (LI_IOSTREAM_DESTROY == event) { + li_stream_simple_socket_close(stream, TRUE); /* kill it, ssl sent an close alert message */ + } + li_connection_simple_tcp(&conctx->con, stream, &conctx->simple_socket_data, event); -static void mod_gnutls_con_handle_events_cb(liJob *job) { - mod_connection_ctx *conctx = LI_CONTAINER_OF(job, mod_connection_ctx, con_handle_events_job); - liConnection *con = conctx->con; + if (NULL != conctx->con && conctx->con->out_has_all_data + && (NULL == stream->stream_out.out || 0 == stream->stream_out.out->length) + && li_streams_empty(conctx->con->con_sock.raw_out, NULL)) { + li_connection_request_done(conctx->con); + } - conctx->gnutls_again_in_progress = 0; - connection_handle_io(con); + switch (event) { + case LI_IOSTREAM_DESTROY: + assert(NULL == conctx->sock_stream); + assert(NULL == conctx->tls_filter); + assert(NULL == conctx->con); + stream->data = NULL; + g_slice_free(mod_connection_ctx, conctx); + return; + default: + break; + } } -static void mod_gnutls_io_cb(struct ev_loop *loop, ev_io *w, int revents) { - liConnection *con = (liConnection*) w->data; - mod_connection_ctx *conctx = con->srv_sock_data; +static void handshake_cb(liGnuTLSFilter *f, gpointer data, liStream *plain_source, liStream *plain_drain) { + mod_connection_ctx *conctx = data; + liConnection *con = conctx->con; + UNUSED(f); - if (revents & EV_ERROR) { - /* if this happens, we have a serious bug in the event handling */ - VR_ERROR(con->mainvr, "%s", "EV_ERROR encountered, dropping connection!"); - li_connection_error(con); - return; + if (NULL != con) { + li_stream_connect(plain_source, con->con_sock.raw_in); + li_stream_connect(con->con_sock.raw_out, plain_drain); + } else { + li_stream_reset(plain_source); + li_stream_reset(plain_drain); } +} - con->can_read = TRUE; - con->can_write = TRUE; +static void close_cb(liGnuTLSFilter *f, gpointer data) { + mod_connection_ctx *conctx = data; + liConnection *con = conctx->con; + assert(conctx->tls_filter == f); + + conctx->tls_filter = NULL; + li_gnutls_filter_free(f); + gnutls_deinit(conctx->session); - /* disable all events; they will get reactivated later */ - li_ev_io_set_events(loop, w, 0); + if (NULL != conctx->ctx) { + mod_gnutls_context_release(conctx->ctx); + conctx->ctx = NULL; + } - li_job_now(&con->wrk->loop.jobqueue, &conctx->con_handle_events_job); + if (NULL != conctx->con) { + liStream *raw_out = con->con_sock.raw_out, *raw_in = con->con_sock.raw_in; + assert(con->con_sock.data == conctx); + conctx->con = NULL; + con->con_sock.data = NULL; + li_stream_acquire(raw_in); + li_stream_reset(raw_out); + li_stream_reset(raw_in); + li_stream_release(raw_in); + } + + if (NULL != conctx->sock_stream) { + liIOStream *stream = conctx->sock_stream; + conctx->sock_stream = NULL; + li_iostream_release(stream); + } } -static int mod_gnutls_post_client_hello_cb(gnutls_session_t session) { - gnutls_protocol_t p = gnutls_protocol_get_version(session); - mod_connection_ctx *conctx = gnutls_session_get_ptr(session); +static int post_client_hello_cb(liGnuTLSFilter *f, gpointer data) { + mod_connection_ctx *conctx = data; + gnutls_protocol_t p = gnutls_protocol_get_version(conctx->session); + UNUSED(f); if (conctx->ctx->protect_against_beast) { if (GNUTLS_SSL3 == p || GNUTLS_TLS1_0 == p) { - gnutls_priority_set(session, conctx->ctx->server_priority_beast); + gnutls_priority_set(conctx->session, conctx->ctx->server_priority_beast); } } return GNUTLS_E_SUCCESS; } -static gboolean mod_gnutls_con_new(liConnection *con) { +static const liGnuTLSFilterCallbacks filter_callbacks = { + handshake_cb, + close_cb, + post_client_hello_cb +}; + +static void gnutlc_tcp_finished(liConnection *con, gboolean aborted) { + mod_connection_ctx *conctx = con->con_sock.data; + UNUSED(aborted); + + con->info.is_ssl = FALSE; + con->con_sock.callbacks = NULL; + + if (NULL != conctx) { + assert(con == conctx->con); + close_cb(conctx->tls_filter, conctx); + } + + { + liStream *raw_out = con->con_sock.raw_out, *raw_in = con->con_sock.raw_in; + con->con_sock.raw_out = con->con_sock.raw_in = NULL; + if (NULL != raw_out) { li_stream_reset(raw_out); li_stream_release(raw_out); } + if (NULL != raw_in) { li_stream_reset(raw_in); li_stream_release(raw_in); } + } +} + +static const liConnectionSocketCallbacks gnutls_tcp_cbs = { + gnutlc_tcp_finished +}; + +static gboolean mod_gnutls_con_new(liConnection *con, int fd) { + liEventLoop *loop = &con->wrk->loop; liServer *srv = con->srv; mod_context *ctx = con->srv_sock->data; - mod_connection_ctx *conctx = g_slice_new0(mod_connection_ctx); + mod_connection_ctx *conctx; + gnutls_session_t session; int r; - con->srv_sock_data = NULL; - - if (GNUTLS_E_SUCCESS != (r = gnutls_init(&conctx->session, GNUTLS_SERVER))) { + if (GNUTLS_E_SUCCESS != (r = gnutls_init(&session, GNUTLS_SERVER))) { ERROR(srv, "gnutls_init (%s): %s", gnutls_strerror_name(r), gnutls_strerror(r)); - g_slice_free(mod_connection_ctx, conctx); return FALSE; } mod_gnutls_context_acquire(ctx); - if (GNUTLS_E_SUCCESS != (r = gnutls_priority_set(conctx->session, ctx->server_priority))) { + if (GNUTLS_E_SUCCESS != (r = gnutls_priority_set(session, ctx->server_priority))) { ERROR(srv, "gnutls_priority_set (%s): %s", gnutls_strerror_name(r), gnutls_strerror(r)); goto fail; } - if (GNUTLS_E_SUCCESS != (r = gnutls_credentials_set(conctx->session, GNUTLS_CRD_CERTIFICATE, ctx->server_cert))) { + if (GNUTLS_E_SUCCESS != (r = gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, ctx->server_cert))) { ERROR(srv, "gnutls_credentials_set (%s): %s", gnutls_strerror_name(r), gnutls_strerror(r)); goto fail; } - gnutls_transport_set_ptr(conctx->session, (gnutls_transport_ptr_t)(intptr_t) con->sock_watcher.fd); - gnutls_session_set_ptr(conctx->session, conctx); - - gnutls_handshake_set_post_client_hello_function(conctx->session, mod_gnutls_post_client_hello_cb); - + conctx = g_slice_new0(mod_connection_ctx); + conctx->session = session; + conctx->sock_stream = li_iostream_new(con->wrk, fd, tcp_io_cb, conctx); + conctx->tls_filter = li_gnutls_filter_new(srv, con->wrk, &filter_callbacks, conctx, conctx->session, + &conctx->sock_stream->stream_in, &conctx->sock_stream->stream_out); conctx->con = con; conctx->ctx = ctx; - li_job_init(&conctx->con_handle_events_job, mod_gnutls_con_handle_events_cb); - conctx->con_events = 0; - conctx->gnutls_again_in_progress = 0; - conctx->initial_handshaked_finished = 0; - conctx->client_initiated_renegotiation = 0; - con->srv_sock_data = conctx; + con->con_sock.data = conctx; + con->con_sock.callbacks = &gnutls_tcp_cbs; + con->con_sock.raw_out = li_stream_plug_new(loop); + con->con_sock.raw_in = li_stream_plug_new(loop); con->info.is_ssl = TRUE; - ev_set_cb(&con->sock_watcher, mod_gnutls_io_cb); - return TRUE; fail: - gnutls_deinit(conctx->session); + gnutls_deinit(session); mod_gnutls_context_release(ctx); - g_slice_free(mod_connection_ctx, conctx); - return FALSE; } -static void mod_gnutls_con_close(liConnection *con) { - mod_connection_ctx *conctx = con->srv_sock_data; - - if (!conctx) return; - - gnutls_bye(conctx->session, GNUTLS_SHUT_RDWR); - gnutls_deinit(conctx->session); - mod_gnutls_context_release(conctx->ctx); - - con->srv_sock_data = NULL; - con->info.is_ssl = FALSE; - li_job_clear(&conctx->con_handle_events_job); - - g_slice_free(mod_connection_ctx, conctx); -} - -static void mod_gnutls_update_events(liConnection *con, int events) { - mod_connection_ctx *conctx = con->srv_sock_data; - - /* new events -> add them to socket watcher too */ - if (!conctx->gnutls_again_in_progress && 0 != (events & ~conctx->con_events)) { - li_ev_io_add_events(con->wrk->loop, &con->sock_watcher, events); - } - - conctx->con_events = events; -} - -static liNetworkStatus mod_gnutls_handle_error(liConnection *con, mod_connection_ctx *conctx, const char *gnutlsfunc, off_t len, int r) { - switch (r) { - case GNUTLS_E_AGAIN: - conctx->gnutls_again_in_progress = TRUE; - li_ev_io_set_events(con->wrk->loop, &con->sock_watcher, gnutls_record_get_direction(conctx->session) ? EV_WRITE : EV_READ); - return (len > 0) ? LI_NETWORK_STATUS_SUCCESS : LI_NETWORK_STATUS_WAIT_FOR_EVENT; - case GNUTLS_E_REHANDSHAKE: - if (conctx->initial_handshaked_finished) { - VR_ERROR(con->mainvr, "%s", "gnutls: client initiated renegotitation, closing connection"); - return LI_NETWORK_STATUS_FATAL_ERROR; - } - break; - case GNUTLS_E_UNEXPECTED_PACKET_LENGTH: - /* connection abort */ - return LI_NETWORK_STATUS_CONNECTION_CLOSE; - case GNUTLS_E_UNKNOWN_CIPHER_SUITE: - case GNUTLS_E_UNSUPPORTED_VERSION_PACKET: - VR_DEBUG(con->mainvr, "%s (%s): %s", gnutlsfunc, - gnutls_strerror_name(r), gnutls_strerror(r)); - return LI_NETWORK_STATUS_CONNECTION_CLOSE; - default: - if (gnutls_error_is_fatal(r)) { - VR_ERROR(con->mainvr, "%s (%s): %s", gnutlsfunc, - gnutls_strerror_name(r), gnutls_strerror(r)); - return LI_NETWORK_STATUS_FATAL_ERROR; - } else { - VR_ERROR(con->mainvr, "%s non fatal (%s): %s", gnutlsfunc, - gnutls_strerror_name(r), gnutls_strerror(r)); - } - } - return (len > 0) ? LI_NETWORK_STATUS_SUCCESS : LI_NETWORK_STATUS_WAIT_FOR_EVENT; -} - -static liNetworkStatus mod_gnutls_do_handshake(liConnection *con, mod_connection_ctx *conctx) { - int r; - - if (GNUTLS_E_SUCCESS != (r = gnutls_handshake(conctx->session))) { - return mod_gnutls_handle_error(con, conctx, "gnutls_handshake", 0, r); - } else { - conctx->initial_handshaked_finished = 1; - return LI_NETWORK_STATUS_SUCCESS; - } -} - -static liNetworkStatus mod_gnutls_con_write(liConnection *con, goffset write_max) { - const ssize_t blocksize = 16*1024; /* 16k */ - char *block_data; - off_t block_len; - ssize_t r; - liChunkIter ci; - liChunkQueue *cq = con->raw_out; - mod_connection_ctx *conctx = con->srv_sock_data; - GError *err = NULL; - - if (!conctx->initial_handshaked_finished) { - liNetworkStatus res = mod_gnutls_do_handshake(con, conctx); - if (res != LI_NETWORK_STATUS_SUCCESS) return res; - } - - do { - if (0 == cq->length) { - return LI_NETWORK_STATUS_SUCCESS; - } - - ci = li_chunkqueue_iter(cq); - switch (li_chunkiter_read(ci, 0, blocksize, &block_data, &block_len, &err)) { - case LI_HANDLER_GO_ON: - break; - case LI_HANDLER_ERROR: - if (NULL != err) { - VR_ERROR(con->mainvr, "Couldn't read data from chunkqueue: %s", err->message); - g_error_free(err); - } - default: - return LI_NETWORK_STATUS_FATAL_ERROR; - } - - if (0 >= (r = gnutls_record_send(conctx->session, block_data, block_len))) { - return mod_gnutls_handle_error(con, conctx, "gnutls_record_send", 0, r); - } - - /* VR_BACKEND_LINES(con->mainvr, block_data, "written %i from %i bytes: ", (int) r, (int) block_len); */ - - li_chunkqueue_skip(cq, r); - write_max -= r; - } while (r == block_len && write_max > 0); - - if (0 != cq->length) { - li_ev_io_add_events(con->wrk->loop, &con->sock_watcher, EV_WRITE); - } - return LI_NETWORK_STATUS_SUCCESS; -} - -static liNetworkStatus mod_gnutls_con_read(liConnection *con) { - liChunkQueue *cq = con->raw_in; - mod_connection_ctx *conctx = con->srv_sock_data; - - const ssize_t blocksize = 16*1024; /* 16k */ - off_t max_read = 16 * blocksize; /* 256k */ - ssize_t r; - off_t len = 0; - - if (!conctx->initial_handshaked_finished) { - liNetworkStatus res = mod_gnutls_do_handshake(con, conctx); - if (res != LI_NETWORK_STATUS_SUCCESS) return res; - } - - if (cq->limit && cq->limit->limit > 0) { - if (max_read > cq->limit->limit - cq->limit->current) { - max_read = cq->limit->limit - cq->limit->current; - if (max_read <= 0) { - max_read = 0; /* we still have to read something */ - VR_ERROR(con->mainvr, "li_network_read: fd %i should be disabled as chunkqueue is already full", - con->sock_watcher.fd); - } - } - } - - do { - liBuffer *buf; - gboolean cq_buf_append; - - buf = li_chunkqueue_get_last_buffer(cq, 1024); - cq_buf_append = (buf != NULL); - - if (buf != NULL) { - /* use last buffer as raw_in_buffer; they should be the same anyway */ - if (G_UNLIKELY(buf != con->raw_in_buffer)) { - li_buffer_acquire(buf); - li_buffer_release(con->raw_in_buffer); - con->raw_in_buffer = buf; - } - } else { - buf = con->raw_in_buffer; - if (buf != NULL && buf->alloc_size - buf->used < 1024) { - /* release *buffer */ - li_buffer_release(buf); - con->raw_in_buffer = buf = NULL; - } - if (buf == NULL) { - con->raw_in_buffer = buf = li_buffer_new(blocksize); - } - } - assert(con->raw_in_buffer == buf); - - if (0 > (r = gnutls_record_recv(conctx->session, buf->addr + buf->used, buf->alloc_size - buf->used))) { - return mod_gnutls_handle_error(con, conctx, "gnutls_record_recv", len, r); - } else if (r == 0) { - return LI_NETWORK_STATUS_CONNECTION_CLOSE; - } - - if (cq_buf_append) { - li_chunkqueue_update_last_buffer_size(cq, r); - } else { - gsize offset; - - li_buffer_acquire(buf); - - offset = buf->used; - buf->used += r; - li_chunkqueue_append_buffer2(cq, buf, offset, r); - } - if (buf->alloc_size - buf->used < 1024) { - /* release *buffer */ - li_buffer_release(buf); - con->raw_in_buffer = buf = NULL; - } - len += r; - } while (len < max_read); - - return LI_NETWORK_STATUS_SUCCESS; -} static void mod_gnutls_sock_release(liServerSocket *srv_sock) { mod_context *ctx = srv_sock->data; @@ -401,7 +269,6 @@ static void mod_gnutls_sock_release(liServerSocket *srv_sock) { mod_gnutls_context_release(ctx); } - static void gnutls_setup_listen_cb(liServer *srv, int fd, gpointer data) { mod_context *ctx = data; liServerSocket *srv_sock; @@ -416,12 +283,8 @@ static void gnutls_setup_listen_cb(liServer *srv, int fd, gpointer data) { srv_sock->data = ctx; /* transfer ownership, no refcount change */ - srv_sock->write_cb = mod_gnutls_con_write; - srv_sock->read_cb = mod_gnutls_con_read; srv_sock->new_cb = mod_gnutls_con_new; - srv_sock->close_cb = mod_gnutls_con_close; srv_sock->release_cb = mod_gnutls_sock_release; - srv_sock->update_events_cb = mod_gnutls_update_events; } static gboolean gnutls_setup(liServer *srv, liPlugin* p, liValue *val, gpointer userdata) {